Table of Contents

Generate a layout class

Beginner · C#. The layout text on a [CStructLayout] class becomes typed classes, Parse, and Serialize at build time; the package ships the generator, so a console project needs nothing else.

Run this example

Prerequisites: the repository's .NET 10 SDK and a checkout of this source. Run from the repository root:

dotnet run --project docs/examples/CStructSharp.Docs.Examples.csproj -c Release -- generated-first-layout

The runner checks Parse into a generated class with kind 2 and length 6, Serialize back to the same six bytes, and the size constant 6. Success includes PASS generated-first-layout.

This example uses the C# API. Browser capabilities and result shapes are described in the browser guide.

Complete program

The layout, options, input bytes, helper methods, and required types are all included. To adapt it outside the repository, create a .NET 10 console project, add CStructSharp, and replace Program.cs with this complete file. These examples follow the source version; use a matching package when testing a release.

Download the complete C# source.

// Generated from executable documentation examples. Edit the source region, then regenerate.
using System;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Buffers;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using CStructSharp;
using CStructSharp.Codecs;
using CStructSharp.Diagnostics;
using CStructSharp.Introspection;
using CStructSharp.Values;

internal static partial class Program
{
    public static void Main()
    {
        GeneratedFirstLayout();
        Console.WriteLine("PASS generated-first-layout");
    }

    private static void GeneratedFirstLayout()
    {
        byte[] bytes = [0x02, 0x00, 0x06, 0x00, 0x00, 0x00];

        // Parse returns the generated class; its properties have the C# types the layout implies.
        Wire.Header header = Wire.Parse(bytes);
        Equal((ushort)2, header.Kind);
        Equal(6u, header.Length);

        // Serialize takes the class back to bytes, and the runtime layout is still there for anything else.
        SequenceEqual(bytes, Wire.Serialize(header));
        Equal("header", Wire.RootName);
        Equal(6, Wire.Layout.GetStructSizeInBytes("header"));
        Equal(6, Wire.Sizes.Header);
    }

    private static void Equal<T>(T expected, T actual)
    {
        if (!EqualityComparer<T>.Default.Equals(expected, actual))
        {
            throw new InvalidOperationException($"Expected '{expected}', received '{actual}'.");
        }
    }

    private static void SequenceEqual(byte[] expected, byte[] actual)
    {
        if (!expected.AsSpan().SequenceEqual(actual))
        {
            throw new InvalidOperationException(
                $"Expected {Convert.ToHexString(expected)}, received {Convert.ToHexString(actual)}.");
        }
    }

    // The layout text lives in the attribute; the generator adds the members of this partial class at build time.
    [CStructLayout("struct header { uint16 kind; uint32 length; };")]
    public static partial class Wire
    {
    }
}

Try it and diagnose mistakes

Add uint8 flags; after length and append a seventh byte.

Answer: Header gains a Flags property, Sizes.Header becomes 7, and the assertions on the six-byte input must change to match. The program contains assertions for its original inputs. When changing an input intentionally, update the expected assertion too; an unchanged assertion is not evidence that the new value is wrong.

Continue with the related guide or choose another recipe.