{"slug":"snowbank-slices-and-buffers","title":"snowbank-slices-and-buffers","summary":"How to correctly use the Slice type and its companions (SliceReader, SliceWriter, SliceOwner) for binary data in the FoundationDB .NET client / SnowBank.Core codebase. Slice is a readonly struct (namespace System) — the logical equivalent of a ReadOnlyMemory of bytes with many he","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-27T20:59:40.346786Z","repo":{"url":"https://github.com/SnowBankSDK/foundationdb-dotnet-client","stars":158,"forks":33,"license":"BSD-3-Clause","updatedAt":"2026-09-26T23:36:54Z"},"bodyHtml":"<hr>\n<h2>name: snowbank-slices-and-buffers\ndescription: How to correctly use the Slice type and its companions (SliceReader, SliceWriter, SliceOwner) for binary data in the FoundationDB .NET client / SnowBank.Core codebase. Slice is a readonly struct (namespace System) — the logical equivalent of a ReadOnlyMemory of bytes with many helpers. Use whenever code constructs or reads a Slice, converts between bytes and other types (Slice.FromBytes/FromStringUtf8/FromInt32/FromFixed64/ToInt64/ToStringUtf8/AsSlice/ToArray), builds or parses a binary buffer (SliceWriter/SliceReader), rents pooled buffers (SliceOwner/ArrayPool), or worries about Nil-vs-Empty, endianness, or which integer encoding to use. For the Span-of-byte (Span-first) equivalents and the low-level buffer/pool machinery, see the bundled reference files.</h2>\n<h1>Slice, SliceReader, SliceWriter &amp; friends</h1>\n<p><code>Slice</code> is the workhorse for binary data in this codebase. It is a <strong><code>readonly struct</code></strong> (in namespace <code>System</code>) that wraps a segment of a <code>byte[]</code> — its three fields are <code>Array</code> (the backing array, possibly null), <code>Offset</code>, and <code>Count</code>. It predates <code>Span&lt;T&gt;</code> and is the logical equivalent of <strong><code>ReadOnlyMemory&lt;byte&gt;</code></strong>, but with a large library of helpers for turning bytes into and out of real-world types. Keys and values in the FoundationDB binding are <code>Slice</code>s.</p>\n<blockquote>\n<p><strong>Two things to internalize first:</strong> (1) a <code>Slice</code> is a <strong>view</strong>, not a copy — it shares the backing array. (2) <code>Slice.Nil</code> (no array) and <code>Slice.Empty</code> (zero-length array) are <strong>different</strong> and the distinction is load-bearing. Both are covered below.</p>\n</blockquote>\n<p>For the Span-first equivalents (<code>SpanReader</code>/<code>SpanWriter</code>, <code>ISpanEncodable</code>) read <a href=\"references/span-readers-writers.md\"><code>references/span-readers-writers.md</code></a>; for pooled buffer-building (<code>ISliceBufferWriter</code>, <code>SlicePool</code>, <code>ValueBuffer&lt;T&gt;</code>, allocators) read <a href=\"references/buffers-and-pooling.md\"><code>references/buffers-and-pooling.md</code></a>.</p>\n<h2>1. Nil vs Empty — the #1 gotcha</h2>\n<table>\n<thead>\n<tr>\n<th></th>\n<th><code>Slice.Nil</code></th>\n<th><code>Slice.Empty</code></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>backing array</td>\n<td>none (null-like)</td>\n<td>a zero-length array</td>\n</tr>\n<tr>\n<td><code>IsNull</code></td>\n<td><code>true</code></td>\n<td><code>false</code></td>\n</tr>\n<tr>\n<td><code>IsEmpty</code></td>\n<td><code>false</code></td>\n<td><code>true</code></td>\n</tr>\n<tr>\n<td><code>IsNullOrEmpty</code></td>\n<td><code>true</code></td>\n<td><code>true</code></td>\n</tr>\n<tr>\n<td><code>IsPresent</code></td>\n<td><code>false</code></td>\n<td><code>true</code></td>\n</tr>\n<tr>\n<td><code>GetBytes()</code></td>\n<td>returns <strong><code>null</code></strong></td>\n<td>returns an <strong>empty array</strong></td>\n</tr>\n<tr>\n<td><code>ToStringUtf8()</code></td>\n<td>returns <strong><code>null</code></strong></td>\n<td>returns <strong><code>\"\"</code></strong></td>\n</tr>\n<tr>\n<td><code>==</code></td>\n<td><code>Nil != Empty</code></td>\n<td>distinct</td>\n</tr>\n<tr>\n<td><code>CompareTo</code></td>\n<td><code>Nil</code> and <code>Empty</code> compare <strong>equal</strong> (both sort first)</td>\n<td></td>\n</tr>\n</tbody>\n</table>\n<p><code>tr.GetAsync(key)</code> returns <strong><code>Slice.Nil</code></strong> for a missing key, so the canonical \"does it exist?\" check is <code>value.IsNull</code> (or <code>IsNullOrEmpty</code> if an empty value also counts as absent). Use <code>Nil</code> to mean <em>absent</em> and <code>Empty</code> to mean <em>present but zero-length</em>.</p>\n<pre><code>var v = await tr.GetAsync(key);\nif (v.IsNull) { /* key does not exist */ }\n</code></pre>\n<h2>2. Slice is a view — copy when you must own it</h2>\n<p>Constructing a <code>Slice</code> from a <code>byte[]</code> does <strong>not</strong> copy; the <code>Slice</code> references the array, so mutations to the array are visible through the slice (and its <code>.Span</code>). When you need an independent owner, copy:</p>\n<pre><code>byte[] buf = ...;\nvar view = buf.AsSlice();        // shares buf — buf[i] = x is visible through view\nbyte[] mine = view.ToArray();    // defensive copy\nbuf[0] = 0xFF;                   // changes `view`, not `mine`\n</code></pre>\n<h2>3. Constructing a Slice</h2>\n<pre><code>// from arrays / spans\nbyte[] b = ...;\nb.AsSlice();                 b.AsSlice(offset, count);\nnew ArraySegment&lt;byte&gt;(b, o, n).AsSlice();\nSlice.FromBytes(\"abc\"u8);    // copies a ReadOnlySpan&lt;byte&gt;\n\n// from text\nSlice.FromStringUtf8(\"héllo\");   Slice.FromString(\"héllo\");   // UTF-8\nSlice.FromStringAscii(\"ABC\");    // ASCII only — lossy/throws on chars &gt; 0x7F\n\n// well-known\nSlice.Empty;   Slice.Nil;   Slice.Zero(16);   // 16 zero bytes\n\n// guids / uuids / hex\nSlice.FromGuid(g);   Slice.FromUuid128(u);   Slice.FromHexString(\"00ff1234\");\n</code></pre>\n<h3>Three integer encodings — pick deliberately</h3>\n<p>This is a classic source of bugs. They are <strong>not</strong> interchangeable:</p>\n<table>\n<thead>\n<tr>\n<th>Factory</th>\n<th>Encoding</th>\n<th>Size (int32)</th>\n<th>Read back with</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Slice.FromInt32(v)</code></td>\n<td>minimal little-endian (leading zero bytes dropped)</td>\n<td>1–4 bytes</td>\n<td><code>slice.ToInt32()</code></td>\n</tr>\n<tr>\n<td><code>Slice.FromFixed32(v)</code></td>\n<td>fixed little-endian</td>\n<td>always 4 bytes</td>\n<td><code>slice.ToInt32()</code></td>\n</tr>\n<tr>\n<td><code>Slice.FromVarint32(v)</code></td>\n<td>7-bit LEB128 varint</td>\n<td>1–5 bytes</td>\n<td>(via <code>SliceReader.ReadVarInt32</code>)</td>\n</tr>\n</tbody>\n</table>\n<p>Every variant has a <strong>big-endian</strong> twin (<code>FromInt32BE</code>, <code>FromFixed32BE</code>, …) and 16/24/64/128-bit widths, plus floats (<code>FromSingle</code>/<code>FromDouble</code>) and <code>FromDecimal</code>. Big-endian fixed encodings are what you want when a number must <strong>sort</strong> correctly as a key. The minimal <code>FromInt32</code> is for standalone values you read whole with <code>ToInt32()</code> — it is <em>not</em> self-delimiting, so don't use it mid-stream (in a <code>SliceWriter</code>, use the fixed-width <code>WriteInt32</code>/<code>WriteInt64</code> or <code>WriteVarInt*</code> there; see §6).</p>\n<blockquote>\n<p>⚠️ <strong>Naming differs between <code>Slice</code> and the writer/reader.</strong> On <code>Slice</code> (standalone), <code>FromFixed32</code> = 4 bytes and <code>FromInt32</code> = minimal. On <code>SliceWriter</code>/<code>SliceReader</code> (streams), the fixed-width method is plain <strong><code>WriteInt32</code>/<code>ReadInt32</code></strong> (4 bytes LE; <code>*BE</code> for big-endian), and the varint is <strong><code>WriteVarInt32</code>/<code>ReadVarInt32</code></strong>. (<code>WriteFixed32</code>/<code>ReadFixed32</code> exist but are <code>[Obsolete]</code> — use <code>WriteInt32</code>/<code>ReadInt32</code>.)</p>\n</blockquote>\n<h2>4. Reading values back</h2>\n<pre><code>slice.ToInt64();   slice.ToInt32BE();   slice.ToGuid();   slice.ToUuid128();\nslice.ToStringUtf8();    // Nil -&gt; null, Empty -&gt; \"\"\nslice.ToArray();         // defensive copy to byte[]\nslice.ToHexString();\n\n// zero-copy access to the bytes\nReadOnlySpan&lt;byte&gt; span = slice.Span;\nReadOnlyMemory&lt;byte&gt; mem = slice.Memory;\n\n// slicing (negative indices count from the end)\nslice.Substring(7, 6);   slice[2..5];   slice[^1..];\n</code></pre>\n<h2>5. Comparison &amp; equality</h2>\n<p><code>Slice</code> compares <strong>lexicographically by raw bytes</strong> (the same order FoundationDB sorts keys), is offset/array-independent (equal content compares equal regardless of backing array or offset), and supports <code>==</code>, <code>&lt;</code>, <code>&gt;</code>, <code>CompareTo</code>, <code>StartsWith</code>, <code>EndsWith</code>, <code>IndexOf</code>. For dictionaries/sorted sets, use <code>Slice.Comparer.Default</code> (an <code>IComparer&lt;Slice&gt;</code> + <code>IEqualityComparer&lt;Slice&gt;</code>).</p>\n<pre><code>a.CompareTo(b) &lt; 0;            // a sorts before b\nkey.StartsWith(prefix);        // prefix match\nvar set = new SortedSet&lt;Slice&gt;(Slice.Comparer.Default);\n</code></pre>\n<h2>6. SliceWriter — build a buffer</h2>\n<p><code>SliceWriter</code> is a <strong>mutable, growable</strong> builder (<code>struct</code>, <code>IBufferWriter&lt;byte&gt;</code>, <code>IDisposable</code>). Start from <code>default(SliceWriter)</code> (heap-backed, grows as needed) or <code>new SliceWriter(pool)</code> (rents from an <code>ArrayPool&lt;byte&gt;</code>):</p>\n<pre><code>var w = new SliceWriter();\nw.WriteInt32(42);                    // fixed 4 bytes LE  (self-delimiting)\nw.WriteVarInt32(1000);               // LEB128            (self-delimiting)\nw.WriteVarString(\"hello\");           // length-prefixed UTF-8\nw.WriteStringUtf8(\"raw\");            // raw UTF-8, NO length prefix\nw.WriteBytes(payload);               // append bytes\nSlice result = w.ToSlice();          // the written region (a view into the writer's buffer)\n</code></pre>\n<ul>\n<li>Use <strong>self-delimiting</strong> writes (fixed-width <code>WriteInt32</code>/<code>WriteInt64</code>/…, <code>WriteVarInt*</code>, <code>WriteVarString</code>) for anything you'll parse back sequentially. A raw <code>WriteStringUtf8</code>/<code>WriteBytes</code> has no length, so the reader must already know the length.</li>\n<li><code>Position</code>, <code>Reset()</code>, <code>Rewind()</code>, <code>Skip(n)</code>, <code>Allocate(n)</code>/<code>AllocateSpan(n)</code> (reserve space to fill in place).</li>\n<li><strong>Pooling caveat:</strong> if you pass an <code>ArrayPool&lt;byte&gt;</code>, you must either <code>Dispose()</code> the writer or hand the buffer off with <code>ToSliceOwner()</code> — otherwise the rented array is never returned. <code>ToSlice()</code> returns a <em>view into the writer's buffer</em>; if the writer (or its pooled buffer) is disposed/reused, that view becomes invalid — <code>ToArray()</code> or <code>ToSliceOwner()</code> it to keep it.</li>\n</ul>\n<h2>7. SliceReader — parse a buffer</h2>\n<p><code>SliceReader</code> is a <strong>forward cursor</strong> over a <code>Slice</code>. Pair each read with the matching write:</p>\n<pre><code>var r = result.ToSliceReader();\nint n     = r.ReadInt32();           // &lt;-&gt; WriteInt32  (fixed 4 bytes)\nuint k    = r.ReadVarInt32();        // &lt;-&gt; WriteVarInt32\nstring s  = r.ReadVarString();       // &lt;-&gt; WriteVarString\n// raw / fixed-length string written without a prefix: read the known number of bytes\nstring raw = r.ReadBytes(3).ToStringUtf8();\nSlice rest = r.ReadToEnd();\n</code></pre>\n<p><code>Remaining</code>, <code>HasMore</code>, <code>Head</code> (bytes already read), <code>Tail</code> (bytes not yet read), and non-advancing <code>PeekByte()</code>/<code>PeekBytes(n)</code> round out the API. There is <strong>no</strong> <code>ReadStringUtf8(n)</code> — use <code>ReadBytes(n).ToStringUtf8()</code>.</p>\n<h2>8. SliceOwner — pooled, disposable Slices</h2>\n<p><code>SliceOwner</code> is a rented <code>Slice</code> that returns its buffer to an <code>ArrayPool&lt;byte&gt;</code> on <code>Dispose</code> — the allocation-free analogue of <code>IMemoryOwner&lt;byte&gt;</code>. The contract: <strong>you MUST <code>Dispose</code> it, and MUST NOT use its data afterward.</strong></p>\n<pre><code>using (var owner = Slice.FromBytes(payload, ArrayPool&lt;byte&gt;.Shared))\n{\n    Slice data = owner.Data;     // valid only inside the using\n    Use(data.Span);\n}   // buffer returned to the pool here\n</code></pre>\n<p><code>owner.IsValid</code>, <code>owner.Count</code>, <code>owner.Span</code>, <code>owner.Pool</code>; <code>SliceOwner.Wrap/Create/Copy</code> and <code>writer.ToSliceOwner()</code> produce them. Don't let an owner's <code>Data</code> escape the <code>using</code>.</p>\n<h2>9. Span / Memory interop &amp; <code>ISpanEncodable</code></h2>\n<p><code>Slice</code> interops freely with the modern primitives: <code>slice.Span</code> (<code>ReadOnlySpan&lt;byte&gt;</code>), <code>slice.Memory</code> (<code>ReadOnlyMemory&lt;byte&gt;</code>), <code>byte[].AsSlice()</code>. Many hot types (keys, values, the writers) implement <strong><code>ISpanEncodable</code></strong> so they can be rendered into a caller's buffer with no intermediate <code>Slice</code> allocation — <code>TryGetSpan(out span)</code> / <code>TryGetSizeHint(out size)</code> / <code>TryEncode(dest, out written)</code>. That interface is how <code>subspace.Key(...)</code>/<code>FdbValue.*</code> write themselves into pooled buffers at the last moment.</p>\n<p>For working directly over <code>Span&lt;byte&gt;</code> (a caller-owned, fixed buffer) instead of <code>Slice</code>, use <code>SpanReader</code>/<code>SpanWriter</code> — see <a href=\"references/span-readers-writers.md\"><code>references/span-readers-writers.md</code></a>.</p>\n<h2>10. Round-trip example</h2>\n<pre><code>// build\nvar w = new SliceWriter();\nw.WriteInt32(order.Id);\nw.WriteVarString(order.Customer);\nw.WriteVarInt64(order.Total);\nSlice packed = w.ToSlice();\n\n// parse\nvar r = packed.ToSliceReader();\nint id        = r.ReadInt32();\nstring cust   = r.ReadVarString();\nlong total    = (long) r.ReadVarInt64();\n</code></pre>\n<h2>11. Self-check</h2>\n<ul>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Did I use <code>IsNull</code>/<code>IsNullOrEmpty</code> (not <code>== Slice.Empty</code>) to test for a missing value?</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Am I treating <code>Slice</code> as a <strong>view</strong> — copying with <code>ToArray()</code>/<code>ToSliceOwner()</code> before mutating shared arrays or outliving a pooled buffer?</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Did I pick the right integer encoding (<code>Fixed*</code>/<code>*BE</code> for sortable keys; <code>VarInt*</code>/<code>Fixed*</code> for self-delimiting stream fields; <code>FromInt32</code> only for standalone whole-slice values)?</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> Do my <code>SliceWriter</code> writes and <code>SliceReader</code> reads pair up (<code>WriteInt32</code>↔<code>ReadInt32</code>, <code>VarInt</code>↔<code>VarInt</code>, <code>VarString</code>↔<code>VarString</code>)?</li>\n<li><input disabled=\"disabled\" type=\"checkbox\"> If I rented from an <code>ArrayPool</code> (<code>SliceWriter(pool)</code> / <code>SliceOwner</code>), did I <code>Dispose</code>/<code>ToSliceOwner()</code> so the buffer returns to the pool — and not use the data after disposal?</li>\n</ul>\n","files":[{"path":"references/buffers-and-pooling.md","sizeBytes":4936,"isText":true},{"path":"references/span-readers-writers.md","sizeBytes":3936,"isText":true},{"path":"SKILL.md","sizeBytes":11132,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-27T21:00:30.552094Z","sha256":"BA832D6E40F8DAE5F696B00B7A0F6157928FB5D61181444E4E14F4ECDC93838A","sizeBytes":8952},"review":null,"source":{"repositoryUrl":"https://github.com/SnowBankSDK/foundationdb-dotnet-client","path":"plugins/foundationdb-skills/skills/snowbank-slices-and-buffers","license":"BSD-3-Clause","commit":"dcbebf1f6f19ddd6b79978c3762f0df18728ccfc","subtreeSha":"D2F3C56D2119CD22F8F8394F090CFACBACCC632694F26A8FDF7F18ACA03AC9B6","lastSyncedAt":"2026-09-27T20:59:38.964495Z"},"reviewedAt":"2026-09-27T21:06:26.551731Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/SnowBankSDK/foundationdb-dotnet-client/tree/master/plugins/foundationdb-skills/skills/snowbank-slices-and-buffers"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install snowbanksdk-foundationdb-dotnet-client@llmmart"},{"target":"git","command":"git clone https://github.com/SnowBankSDK/foundationdb-dotnet-client.git"}]}