Register a custom codec
Advanced · C#. An ICustomCodec supplies the name, size, alignment, reader, and writer of a caller-defined type; the layout uses it like any primitive, including as an array count.
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 -- custom-codec
The runner checks varint fields decoded as 2, 128, and 5 with an exact round trip and address 4 for the second id. Success includes PASS custom-codec.
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()
{
CustomCodec();
Console.WriteLine("PASS custom-codec");
}
private static void CustomCodec()
{
// A caller-defined type: a protobuf-style varint registered under the name a definition uses.
var codecs = new List<ICustomCodec> { new Varint() };
var layout = new CStruct(
"struct entry { varint id; uint8 kind; }; struct root { varint count; entry items[count]; };",
compilationOptions: new CStructCompilationOptions { Codecs = codecs });
byte[] bytes = [2, 0x80, 0x01, 7, 0x05, 9];
StructValue root = layout.Parse(bytes, "root");
Equal(128UL, root.Get<ulong>("items[0].id"));
Equal((byte)9, root.Get<byte>("items[1].kind"));
SequenceEqual(bytes, layout.Serialize("root", root));
using var stream = new MemoryStream(bytes);
Equal(4L, layout.ResolveAddress(stream, "root.items[1].id"));
}
private sealed class Varint : ICustomCodec
{
public string Name => "varint";
public int? FixedSize => null;
public int Alignment => 1;
// The library hands the codec the bytes from the value's start: all remaining input for memory sources,
// a growing window for other streams. The codec says how many it used.
public OperationStatus Read(ReadOnlySpan<byte> source, out object? value, out int bytesConsumed)
{
ulong result = 0;
for (int index = 0; index < source.Length && index < 10; index++)
{
result |= (ulong)(source[index] & 0x7F) << (7 * index);
if ((source[index] & 0x80) == 0)
{
value = result;
bytesConsumed = index + 1;
return OperationStatus.Done;
}
}
value = null;
bytesConsumed = 0;
return source.Length >= 10 ? OperationStatus.InvalidData : OperationStatus.NeedMoreData;
}
// DestinationTooSmall asks for a larger window; ten bytes always suffice for a 64-bit varint.
public OperationStatus Write(Span<byte> destination, object value, out int bytesWritten)
{
ulong remaining = Convert.ToUInt64(value, CultureInfo.InvariantCulture);
bytesWritten = 0;
do
{
if (bytesWritten == destination.Length)
{
return OperationStatus.DestinationTooSmall;
}
byte next = (byte)(remaining & 0x7F);
remaining >>= 7;
destination[bytesWritten++] = remaining == 0 ? next : (byte)(next | 0x80);
}
while (remaining != 0);
return OperationStatus.Done;
}
}
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)}.");
}
}
}
Try it and diagnose mistakes
Change the second varint from 80 01 to 81 01.
Answer: id becomes 129 (0x81 & 0x7F plus 1 << 7); the field still occupies two bytes. 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.