Map a generated layout to your classes
Intermediate · C#. A [CStructMapped] partial class gets ReadFrom, WriteTo, and its registration from the generator; properties match members exactly, then case-insensitively, then ignoring underscores, or by [CStructMember].
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-mapped-classes
The runner checks a [CStructMapped] record read by the runtime and by the generated bridge, an exact SerializeMapped round trip, and name matching through [CStructMember]. Success includes PASS generated-mapped-classes.
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()
{
GeneratedMappedClasses();
Console.WriteLine("PASS generated-mapped-classes");
}
private static void GeneratedMappedClasses()
{
byte[] bytes = [0x02, 0x00, 0x06, 0x00, 0x00, 0x00];
// The layout class reads its root straight into the mapped class; Wire.Layout.ReadValue<T>(bytes, "header")
// is the same call with the root named, and the generated instance bridges to it as well.
HeaderRecord record = Wire.ReadValue<HeaderRecord>(bytes);
Equal(6u, record.Length);
Equal(6u, Wire.Layout.ReadValue<HeaderRecord>(bytes, "header").Length);
Equal(6u, Wire.ToMapped<HeaderRecord>(Wire.Parse(bytes)).Length);
True(!Wire.TryReadValue(bytes[..3], out HeaderRecord? _), "three bytes are not a header");
SequenceEqual(bytes, Wire.SerializeMapped(record));
// Names match by exact spelling, then case-insensitively, then ignoring underscores; [CStructMember] overrides.
byte[] sampleBytes = [2, 0x34, 0x12, 0x78, 0x56, (byte)'p', (byte)'n', (byte)'g', 0, 0, 0, 0, 0, 0xC3, 0xA9, (byte)'t', (byte)'e', 0, 0, (byte)'o', (byte)'k', 0, 2, 7];
SampleRecord sample = Samples.Layout.ReadValue<SampleRecord>(sampleBytes, "sample");
Equal(2, sample.Values.Count);
Equal("png\0\0\0\0\0", sample.FileName);
Equal(Samples.Color.Green, sample.Colour);
}
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 True(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
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
{
}
[CStructLayout("""
enum color : uint8 { Red = 1, Green = 2, Blue = 3 };
flag perms : uint8 { Read = 1, Write = 2, Execute = 4 };
struct sample {
uint8 count;
uint16 values[count];
char name[8];
utf8 label[6];
cstring note;
color colour;
perms mode;
};
""", Root = "sample")]
public static partial class Samples
{
}
// The generator writes ReadFrom, WriteTo, and the registration; the properties are matched by name.
[CStructMapped(Layout = "header")]
public sealed partial class HeaderRecord
{
public ushort Kind { get; set; }
public uint Length { get; set; }
}
[CStructMapped]
public sealed partial class SampleRecord
{
public byte Count { get; set; }
public List<ushort> Values { get; set; } = [];
[CStructMember("name")]
public string FileName { get; set; } = string.Empty;
public Samples.Color Colour { get; set; }
}
}
Try it and diagnose mistakes
Rename FileName to Name and drop its [CStructMember] attribute.
Answer: The property still maps: Name matches the layout's name case-insensitively. 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.