snowbank-slices-and-buffers
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
Install
npx skills add https://github.com/SnowBankSDK/foundationdb-dotnet-client/tree/master/.claude/skills/snowbank-slices-and-buffers
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install snowbanksdk-foundationdb-dotnet-client@llmmart
git clone https://github.com/SnowBankSDK/foundationdb-dotnet-client.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole snowbanksdk/foundationdb-dotnet-client collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Slice, SliceReader, SliceWriter & friends
Slice is the workhorse for binary data in this codebase. It is a readonly struct (in namespace System) that wraps a segment of a byte[] — its three fields are Array (the backing array, possibly null), Offset, and Count. It predates Span<T> and is the logical equivalent of ReadOnlyMemory<byte>, 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 Slices.
Two things to internalize first: (1) a
Sliceis a view, not a copy — it shares the backing array. (2)Slice.Nil(no array) andSlice.Empty(zero-length array) are different and the distinction is load-bearing. Both are covered below.
For the Span-first equivalents (SpanReader/SpanWriter, ISpanEncodable) read references/span-readers-writers.md; for pooled buffer-building (ISliceBufferWriter, SlicePool, ValueBuffer<T>, allocators) read references/buffers-and-pooling.md.
1. Nil vs Empty — the #1 gotcha
Slice.Nil |
Slice.Empty |
|
|---|---|---|
| backing array | none (null-like) | a zero-length array |
IsNull |
true |
false |
IsEmpty |
false |
true |
IsNullOrEmpty |
true |
true |
IsPresent |
false |
true |
GetBytes() |
returns null |
returns an empty array |
ToStringUtf8() |
returns null |
returns "" |
== |
Nil != Empty |
distinct |
CompareTo |
Nil and Empty compare equal (both sort first) |
tr.GetAsync(key) returns Slice.Nil for a missing key, so the canonical "does it exist?" check is value.IsNull (or IsNullOrEmpty if an empty value also counts as absent). Use Nil to mean absent and Empty to mean present but zero-length.
var v = await tr.GetAsync(key);
if (v.IsNull) { /* key does not exist */ }
2. Slice is a view — copy when you must own it
Constructing a Slice from a byte[] does not copy; the Slice references the array, so mutations to the array are visible through the slice (and its .Span). When you need an independent owner, copy:
byte[] buf = ...;
var view = buf.AsSlice(); // shares buf — buf[i] = x is visible through view
byte[] mine = view.ToArray(); // defensive copy
buf[0] = 0xFF; // changes `view`, not `mine`
3. Constructing a Slice
// from arrays / spans
byte[] b = ...;
b.AsSlice(); b.AsSlice(offset, count);
new ArraySegment<byte>(b, o, n).AsSlice();
Slice.FromBytes("abc"u8); // copies a ReadOnlySpan<byte>
// from text
Slice.FromStringUtf8("héllo"); Slice.FromString("héllo"); // UTF-8
Slice.FromStringAscii("ABC"); // ASCII only — lossy/throws on chars > 0x7F
// well-known
Slice.Empty; Slice.Nil; Slice.Zero(16); // 16 zero bytes
// guids / uuids / hex
Slice.FromGuid(g); Slice.FromUuid128(u); Slice.FromHexString("00ff1234");
Three integer encodings — pick deliberately
This is a classic source of bugs. They are not interchangeable:
| Factory | Encoding | Size (int32) | Read back with |
|---|---|---|---|
Slice.FromInt32(v) |
minimal little-endian (leading zero bytes dropped) | 1–4 bytes | slice.ToInt32() |
Slice.FromFixed32(v) |
fixed little-endian | always 4 bytes | slice.ToInt32() |
Slice.FromVarint32(v) |
7-bit LEB128 varint | 1–5 bytes | (via SliceReader.ReadVarInt32) |
Every variant has a big-endian twin (FromInt32BE, FromFixed32BE, …) and 16/24/64/128-bit widths, plus floats (FromSingle/FromDouble) and FromDecimal. Big-endian fixed encodings are what you want when a number must sort correctly as a key. The minimal FromInt32 is for standalone values you read whole with ToInt32() — it is not self-delimiting, so don't use it mid-stream (in a SliceWriter, use the fixed-width WriteInt32/WriteInt64 or WriteVarInt* there; see §6).
⚠️ Naming differs between
Sliceand the writer/reader. OnSlice(standalone),FromFixed32= 4 bytes andFromInt32= minimal. OnSliceWriter/SliceReader(streams), the fixed-width method is plainWriteInt32/ReadInt32(4 bytes LE;*BEfor big-endian), and the varint isWriteVarInt32/ReadVarInt32. (WriteFixed32/ReadFixed32exist but are[Obsolete]— useWriteInt32/ReadInt32.)
4. Reading values back
slice.ToInt64(); slice.ToInt32BE(); slice.ToGuid(); slice.ToUuid128();
slice.ToStringUtf8(); // Nil -> null, Empty -> ""
slice.ToArray(); // defensive copy to byte[]
slice.ToHexString();
// zero-copy access to the bytes
ReadOnlySpan<byte> span = slice.Span;
ReadOnlyMemory<byte> mem = slice.Memory;
// slicing (negative indices count from the end)
slice.Substring(7, 6); slice[2..5]; slice[^1..];
5. Comparison & equality
Slice compares lexicographically by raw bytes (the same order FoundationDB sorts keys), is offset/array-independent (equal content compares equal regardless of backing array or offset), and supports ==, <, >, CompareTo, StartsWith, EndsWith, IndexOf. For dictionaries/sorted sets, use Slice.Comparer.Default (an IComparer<Slice> + IEqualityComparer<Slice>).
a.CompareTo(b) < 0; // a sorts before b
key.StartsWith(prefix); // prefix match
var set = new SortedSet<Slice>(Slice.Comparer.Default);
6. SliceWriter — build a buffer
SliceWriter is a mutable, growable builder (struct, IBufferWriter<byte>, IDisposable). Start from default(SliceWriter) (heap-backed, grows as needed) or new SliceWriter(pool) (rents from an ArrayPool<byte>):
var w = new SliceWriter();
w.WriteInt32(42); // fixed 4 bytes LE (self-delimiting)
w.WriteVarInt32(1000); // LEB128 (self-delimiting)
w.WriteVarString("hello"); // length-prefixed UTF-8
w.WriteStringUtf8("raw"); // raw UTF-8, NO length prefix
w.WriteBytes(payload); // append bytes
Slice result = w.ToSlice(); // the written region (a view into the writer's buffer)
- Use self-delimiting writes (fixed-width
WriteInt32/WriteInt64/…,WriteVarInt*,WriteVarString) for anything you'll parse back sequentially. A rawWriteStringUtf8/WriteByteshas no length, so the reader must already know the length. Position,Reset(),Rewind(),Skip(n),Allocate(n)/AllocateSpan(n)(reserve space to fill in place).- Pooling caveat: if you pass an
ArrayPool<byte>, you must eitherDispose()the writer or hand the buffer off withToSliceOwner()— otherwise the rented array is never returned.ToSlice()returns a view into the writer's buffer; if the writer (or its pooled buffer) is disposed/reused, that view becomes invalid —ToArray()orToSliceOwner()it to keep it.
7. SliceReader — parse a buffer
SliceReader is a forward cursor over a Slice. Pair each read with the matching write:
var r = result.ToSliceReader();
int n = r.ReadInt32(); // <-> WriteInt32 (fixed 4 bytes)
uint k = r.ReadVarInt32(); // <-> WriteVarInt32
string s = r.ReadVarString(); // <-> WriteVarString
// raw / fixed-length string written without a prefix: read the known number of bytes
string raw = r.ReadBytes(3).ToStringUtf8();
Slice rest = r.ReadToEnd();
Remaining, HasMore, Head (bytes already read), Tail (bytes not yet read), and non-advancing PeekByte()/PeekBytes(n) round out the API. There is no ReadStringUtf8(n) — use ReadBytes(n).ToStringUtf8().
8. SliceOwner — pooled, disposable Slices
SliceOwner is a rented Slice that returns its buffer to an ArrayPool<byte> on Dispose — the allocation-free analogue of IMemoryOwner<byte>. The contract: you MUST Dispose it, and MUST NOT use its data afterward.
using (var owner = Slice.FromBytes(payload, ArrayPool<byte>.Shared))
{
Slice data = owner.Data; // valid only inside the using
Use(data.Span);
} // buffer returned to the pool here
owner.IsValid, owner.Count, owner.Span, owner.Pool; SliceOwner.Wrap/Create/Copy and writer.ToSliceOwner() produce them. Don't let an owner's Data escape the using.
9. Span / Memory interop & ISpanEncodable
Slice interops freely with the modern primitives: slice.Span (ReadOnlySpan<byte>), slice.Memory (ReadOnlyMemory<byte>), byte[].AsSlice(). Many hot types (keys, values, the writers) implement ISpanEncodable so they can be rendered into a caller's buffer with no intermediate Slice allocation — TryGetSpan(out span) / TryGetSizeHint(out size) / TryEncode(dest, out written). That interface is how subspace.Key(...)/FdbValue.* write themselves into pooled buffers at the last moment.
For working directly over Span<byte> (a caller-owned, fixed buffer) instead of Slice, use SpanReader/SpanWriter — see references/span-readers-writers.md.
10. Round-trip example
// build
var w = new SliceWriter();
w.WriteInt32(order.Id);
w.WriteVarString(order.Customer);
w.WriteVarInt64(order.Total);
Slice packed = w.ToSlice();
// parse
var r = packed.ToSliceReader();
int id = r.ReadInt32();
string cust = r.ReadVarString();
long total = (long) r.ReadVarInt64();
11. Self-check
- Did I use
IsNull/IsNullOrEmpty(not== Slice.Empty) to test for a missing value? - Am I treating
Sliceas a view — copying withToArray()/ToSliceOwner()before mutating shared arrays or outliving a pooled buffer? - Did I pick the right integer encoding (
Fixed*/*BEfor sortable keys;VarInt*/Fixed*for self-delimiting stream fields;FromInt32only for standalone whole-slice values)? - Do my
SliceWriterwrites andSliceReaderreads pair up (WriteInt32↔ReadInt32,VarInt↔VarInt,VarString↔VarString)? - If I rented from an
ArrayPool(SliceWriter(pool)/SliceOwner), did IDispose/ToSliceOwner()so the buffer returns to the pool — and not use the data after disposal?
Files (foundationdb-dotnet-client)
-
references
-
buffers-and-pooling.md 4.8 KB
# Buffers, writers & pooling (allocation-conscious) Allocation-consciousness is a core value of this codebase, so there's a family of buffer-builders and pools beyond `SliceWriter`. Reach for these when profiling shows allocation pressure (lots of short-lived keys/values, hot serialization loops); for everyday work `SliceWriter` + `SliceOwner` (see `SKILL.md`) are enough. > **Disposal discipline:** every pooled type here is `IDisposable` and **must** be disposed (or its slabs/arrays never return to the pool, which *degrades* pool performance for everyone). Pair each with `using`. ## `SliceOwner` — a rented Slice (recap) The result of `SliceWriter.ToSliceOwner()`, `Slice.FromBytes(span, pool)`, or `SliceOwner.Create/Copy/Wrap`. It owns a (possibly pooled) buffer and returns it on `Dispose`. `Data`/`Span`/`Count`/`IsValid`/`Pool`. **MUST** be disposed; its data **MUST NOT** be used afterward. This is the type you return from an API that produces bytes but wants to stay allocation-free. ## `ISliceBufferWriter` — `IBufferWriter<byte>` that also vends Slices `ISliceBufferWriter : IBufferWriter<byte>` adds `GetSlice(...)` (returns an `ArraySegment<byte>`) on top of the standard `GetSpan(sizeHint)` / `Advance(count)` protocol. Three implementations, differing in how they manage memory: | Type | Memory | Use when | |---|---|---| | `ArraySliceWriter` | one **contiguous** heap array, grown by copying | you need the final bytes contiguous and don't want pooling | | `SlabSliceWriter` | **slabs** from a pool (or heap), kept until `Dispose`/clear; **not** contiguous | high throughput, you consume per-chunk and don't need one span | | `PooledSliceWriter` | one contiguous array **rented from a pool**, grown by copying | contiguous output *and* pooling | All follow the standard writer protocol and integrate with anything that takes an `IBufferWriter<byte>` (e.g. `Utf8JsonWriter`): ```csharp using var w = new SlabSliceWriter(); Span<byte> span = w.GetSpan(64); // request space // ... write into span ... w.Advance(written); // commit it // consume via w.GetSlice(...) / the IBufferWriter surface; Dispose returns slabs to the pool ``` ## `ISliceAllocator` — many short-lived slices (an arena) When you allocate a *lot* of slices that won't outlive a single operation (e.g. building all the keys for one transaction), an allocator is faster than repeatedly growing a `SliceWriter`. Instead of N independent array allocations you allocate a few big slabs, sub-allocate from them, then release them all together: ```csharp using var alloc = new ArraySliceAllocator(); // ISliceAllocator : IDisposable ArraySegment<byte> seg = alloc.Allocate(16); // carved from a shared slab // ... fill seg.AsSpan() ... ``` - **`ArraySliceAllocator`** — slabs from the heap. - **`PooledSliceAllocator`** — slabs rented from an `ArrayPool`; **must** be disposed to return them (failing to dispose *hurts* pool performance for everyone). Both implement `ISliceAllocator : IDisposable` with `Allocate(int) → ArraySegment<byte>`. The mental model is a per-request / per-transaction arena. > The older **`SlicePool`** type is `[Obsolete]` — use an `ISliceAllocator` (above) or an `ISliceBufferWriter` instead. ## `ValueBuffer<T>` / `SegmentedValueBuffer<T>` / `PooledBuffer<T>` — accumulators Value-type, growable accumulators — a `List<T>` you can seed with stack memory and that avoids heap allocation until it has to: ```csharp // seed with stack space; only spills to a pooled array if it outgrows the seed using var buf = new ValueBuffer<int>(stackalloc int[16]); // or new ValueBuffer<int>(capacity) buf.Add(1); buf.AddRange(more); Span<int> items = buf.GetSpan(); // contiguous view of everything added int[] copy = buf.ToArray(); ``` - **`ValueBuffer<T>`** (`ref struct`) — final items are a **single contiguous** `Span<T>`. Great for "collect an unknown number of items, then process them once." - **`SegmentedValueBuffer<T>`** (`ref struct`) — same idea but **segmented**; faster when you *don't* need one contiguous span (you iterate the segments). - **`PooledBuffer<T>`** (`struct`, `IBufferWriter<T>` + `IDisposable`) — a pooled accumulator usable as an `IBufferWriter<T>` (so it plugs into APIs that write into one). Dispose to return the rented array. ## Choosing, in one line - Build a key/value and hand it off → **`SliceWriter` → `ToSlice()`/`ToSliceOwner()`**. - Write into a buffer you already hold, on the stack → **`SpanWriter`** (see the Span reference). - Build *many* throwaway slices in one operation → **`ISliceAllocator`** (`ArraySliceAllocator` / `PooledSliceAllocator`). - Accumulate an unknown number of items, then process once → **`ValueBuffer<T>`** (contiguous) or **`SegmentedValueBuffer<T>`** (segmented). - Need an `IBufferWriter<byte>` for some other API → **`SlabSliceWriter` / `ArraySliceWriter` / `PooledBuffer<T>`**. -
span-readers-writers.md 3.8 KB
# Span-first reading & writing `Slice`/`SliceReader`/`SliceWriter` are the everyday types (see `SKILL.md`). When you already hold a **caller-owned `Span<byte>`/`ReadOnlySpan<byte>`** — a `stackalloc` buffer, a rented array, or a destination handed to you — work over it directly with the `Span*` equivalents. They are **`ref struct`s** (stack-only, zero allocation) and do **not** own or grow memory. ## SpanReader — parse a `ReadOnlySpan<byte>` A forward cursor over a fixed span. Same shape as `SliceReader`, but returns `ReadOnlySpan<byte>` slices instead of `Slice` and cannot escape the stack. ```csharp var r = new SpanReader(span); // span is ReadOnlySpan<byte> int n = r.ReadInt32(); // also ReadInt32BE, ReadInt16/24/64, ReadUInt*, ReadSingle/Double ReadOnlySpan<byte> four = r.ReadFourBytes(); // ReadTwoBytes / ReadFourBytes / ReadEightBytes / ReadSixteenBytes Guid g = r.ReadGuid(); // ReadUuid128 / ReadUuid64 var rest = r.ReadToEnd(); // non-advancing: r.PeekByte(), r.PeekBytes(n); cursor state via r.Remaining / r.Position ``` ## SpanWriter — write into a `Span<byte>` Writes into either a caller buffer or an internally-sized one; `ToSpan()` returns the written region. It will **not grow** a caller-provided buffer — it throws if you overflow it (that's the point: bounded, allocation-free). ```csharp Span<byte> scratch = stackalloc byte[64]; var w = new SpanWriter(scratch); // or new SpanWriter(capacity) w.WriteInt32(42); // fixed-width LE; *BE variants for big-endian w.WriteUInt64BE(version); w.WriteByte(0xFF); w.WriteBytes(payload); Span<byte> room = w.Allocate(16); // reserve, fill in place ReadOnlySpan<byte> written = w.ToSpan(); ``` There are also `static` `SpanWriter.WriteInt32(span, value)`-style helpers when you just need to poke a value into a span at a known position without a cursor. ## When to prefer Span-first vs Slice-based - **`SpanReader`/`SpanWriter`** — you have a fixed, caller-owned buffer and want zero allocation, and the work stays on the stack. Ideal inside `ISpanEncodable.TryEncode`, parsers, and hot loops. - **`SliceReader`/`SliceWriter`** — you need the buffer to **grow**, or to **hand the result off** as a `Slice` that outlives the current stack frame (store it, return it, put it in a value). `SliceWriter` can rent/grow and produce a `Slice`/`SliceOwner`; a `ref struct` `SpanWriter` cannot. ## The `ISpanEncodable` contract Hot types (keys, values, the writers) implement **`ISpanEncodable`** so they can render themselves into a caller's buffer with no intermediate `Slice`: ```csharp public interface ISpanEncodable { bool TryGetSpan(out ReadOnlySpan<byte> span); // already-contiguous? hand it over (zero copy) bool TryGetSizeHint(out int sizeHint); // how big will TryEncode need? bool TryEncode(Span<byte> destination, out int bytesWritten); // write into the caller's buffer } ``` This is exactly how `subspace.Key(...)` and `FdbValue.*` get rendered into pooled buffers at the last moment, instead of allocating a `Slice` per key. When you write your own key/value type, implementing `ISpanEncodable` lets it participate in that zero-allocation path. ## Typed span encoders/decoders - **`SpanDecoderExtensions`** — extension methods to decode a `ReadOnlySpan<byte>` straight into types (`ToInt64`, `ToGuid`, …): the span-based mirror of `Slice.ToXxx`. Use when you have a span and don't want to wrap it in a `Slice` first. - **`ISpanEncoder<T>` / `ISpanDecoder<T>`** and the static **`SpanEncoders`** (in `SnowBank.Data.Binary`) — the strategy types the value layer uses to encode/decode `T ⇄ bytes` generically (e.g. `FdbValue<T, TEncoder>`). You rarely call these directly, but they're what makes `FdbValue.ToFixed64LittleEndian`/`ToTextUtf8`/etc. allocation-free and composable.
-
-
SKILL.md 10.9 KB
--- name: snowbank-slices-and-buffers description: 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. --- # Slice, SliceReader, SliceWriter & friends `Slice` is the workhorse for binary data in this codebase. It is a **`readonly struct`** (in namespace `System`) that wraps a segment of a `byte[]` — its three fields are `Array` (the backing array, possibly null), `Offset`, and `Count`. It predates `Span<T>` and is the logical equivalent of **`ReadOnlyMemory<byte>`**, 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 `Slice`s. > **Two things to internalize first:** (1) a `Slice` is a **view**, not a copy — it shares the backing array. (2) `Slice.Nil` (no array) and `Slice.Empty` (zero-length array) are **different** and the distinction is load-bearing. Both are covered below. For the Span-first equivalents (`SpanReader`/`SpanWriter`, `ISpanEncodable`) read [`references/span-readers-writers.md`](references/span-readers-writers.md); for pooled buffer-building (`ISliceBufferWriter`, `SlicePool`, `ValueBuffer<T>`, allocators) read [`references/buffers-and-pooling.md`](references/buffers-and-pooling.md). ## 1. Nil vs Empty — the #1 gotcha | | `Slice.Nil` | `Slice.Empty` | |---|---|---| | backing array | none (null-like) | a zero-length array | | `IsNull` | `true` | `false` | | `IsEmpty` | `false` | `true` | | `IsNullOrEmpty` | `true` | `true` | | `IsPresent` | `false` | `true` | | `GetBytes()` | returns **`null`** | returns an **empty array** | | `ToStringUtf8()` | returns **`null`** | returns **`""`** | | `==` | `Nil != Empty` | distinct | | `CompareTo` | `Nil` and `Empty` compare **equal** (both sort first) | `tr.GetAsync(key)` returns **`Slice.Nil`** for a missing key, so the canonical "does it exist?" check is `value.IsNull` (or `IsNullOrEmpty` if an empty value also counts as absent). Use `Nil` to mean *absent* and `Empty` to mean *present but zero-length*. ```csharp var v = await tr.GetAsync(key); if (v.IsNull) { /* key does not exist */ } ``` ## 2. Slice is a view — copy when you must own it Constructing a `Slice` from a `byte[]` does **not** copy; the `Slice` references the array, so mutations to the array are visible through the slice (and its `.Span`). When you need an independent owner, copy: ```csharp byte[] buf = ...; var view = buf.AsSlice(); // shares buf — buf[i] = x is visible through view byte[] mine = view.ToArray(); // defensive copy buf[0] = 0xFF; // changes `view`, not `mine` ``` ## 3. Constructing a Slice ```csharp // from arrays / spans byte[] b = ...; b.AsSlice(); b.AsSlice(offset, count); new ArraySegment<byte>(b, o, n).AsSlice(); Slice.FromBytes("abc"u8); // copies a ReadOnlySpan<byte> // from text Slice.FromStringUtf8("héllo"); Slice.FromString("héllo"); // UTF-8 Slice.FromStringAscii("ABC"); // ASCII only — lossy/throws on chars > 0x7F // well-known Slice.Empty; Slice.Nil; Slice.Zero(16); // 16 zero bytes // guids / uuids / hex Slice.FromGuid(g); Slice.FromUuid128(u); Slice.FromHexString("00ff1234"); ``` ### Three integer encodings — pick deliberately This is a classic source of bugs. They are **not** interchangeable: | Factory | Encoding | Size (int32) | Read back with | |---|---|---|---| | `Slice.FromInt32(v)` | minimal little-endian (leading zero bytes dropped) | 1–4 bytes | `slice.ToInt32()` | | `Slice.FromFixed32(v)` | fixed little-endian | always 4 bytes | `slice.ToInt32()` | | `Slice.FromVarint32(v)` | 7-bit LEB128 varint | 1–5 bytes | (via `SliceReader.ReadVarInt32`) | Every variant has a **big-endian** twin (`FromInt32BE`, `FromFixed32BE`, …) and 16/24/64/128-bit widths, plus floats (`FromSingle`/`FromDouble`) and `FromDecimal`. Big-endian fixed encodings are what you want when a number must **sort** correctly as a key. The minimal `FromInt32` is for standalone values you read whole with `ToInt32()` — it is *not* self-delimiting, so don't use it mid-stream (in a `SliceWriter`, use the fixed-width `WriteInt32`/`WriteInt64` or `WriteVarInt*` there; see §6). > ⚠️ **Naming differs between `Slice` and the writer/reader.** On `Slice` (standalone), `FromFixed32` = 4 bytes and `FromInt32` = minimal. On `SliceWriter`/`SliceReader` (streams), the fixed-width method is plain **`WriteInt32`/`ReadInt32`** (4 bytes LE; `*BE` for big-endian), and the varint is **`WriteVarInt32`/`ReadVarInt32`**. (`WriteFixed32`/`ReadFixed32` exist but are `[Obsolete]` — use `WriteInt32`/`ReadInt32`.) ## 4. Reading values back ```csharp slice.ToInt64(); slice.ToInt32BE(); slice.ToGuid(); slice.ToUuid128(); slice.ToStringUtf8(); // Nil -> null, Empty -> "" slice.ToArray(); // defensive copy to byte[] slice.ToHexString(); // zero-copy access to the bytes ReadOnlySpan<byte> span = slice.Span; ReadOnlyMemory<byte> mem = slice.Memory; // slicing (negative indices count from the end) slice.Substring(7, 6); slice[2..5]; slice[^1..]; ``` ## 5. Comparison & equality `Slice` compares **lexicographically by raw bytes** (the same order FoundationDB sorts keys), is offset/array-independent (equal content compares equal regardless of backing array or offset), and supports `==`, `<`, `>`, `CompareTo`, `StartsWith`, `EndsWith`, `IndexOf`. For dictionaries/sorted sets, use `Slice.Comparer.Default` (an `IComparer<Slice>` + `IEqualityComparer<Slice>`). ```csharp a.CompareTo(b) < 0; // a sorts before b key.StartsWith(prefix); // prefix match var set = new SortedSet<Slice>(Slice.Comparer.Default); ``` ## 6. SliceWriter — build a buffer `SliceWriter` is a **mutable, growable** builder (`struct`, `IBufferWriter<byte>`, `IDisposable`). Start from `default(SliceWriter)` (heap-backed, grows as needed) or `new SliceWriter(pool)` (rents from an `ArrayPool<byte>`): ```csharp var w = new SliceWriter(); w.WriteInt32(42); // fixed 4 bytes LE (self-delimiting) w.WriteVarInt32(1000); // LEB128 (self-delimiting) w.WriteVarString("hello"); // length-prefixed UTF-8 w.WriteStringUtf8("raw"); // raw UTF-8, NO length prefix w.WriteBytes(payload); // append bytes Slice result = w.ToSlice(); // the written region (a view into the writer's buffer) ``` - Use **self-delimiting** writes (fixed-width `WriteInt32`/`WriteInt64`/…, `WriteVarInt*`, `WriteVarString`) for anything you'll parse back sequentially. A raw `WriteStringUtf8`/`WriteBytes` has no length, so the reader must already know the length. - `Position`, `Reset()`, `Rewind()`, `Skip(n)`, `Allocate(n)`/`AllocateSpan(n)` (reserve space to fill in place). - **Pooling caveat:** if you pass an `ArrayPool<byte>`, you must either `Dispose()` the writer or hand the buffer off with `ToSliceOwner()` — otherwise the rented array is never returned. `ToSlice()` returns a *view into the writer's buffer*; if the writer (or its pooled buffer) is disposed/reused, that view becomes invalid — `ToArray()` or `ToSliceOwner()` it to keep it. ## 7. SliceReader — parse a buffer `SliceReader` is a **forward cursor** over a `Slice`. Pair each read with the matching write: ```csharp var r = result.ToSliceReader(); int n = r.ReadInt32(); // <-> WriteInt32 (fixed 4 bytes) uint k = r.ReadVarInt32(); // <-> WriteVarInt32 string s = r.ReadVarString(); // <-> WriteVarString // raw / fixed-length string written without a prefix: read the known number of bytes string raw = r.ReadBytes(3).ToStringUtf8(); Slice rest = r.ReadToEnd(); ``` `Remaining`, `HasMore`, `Head` (bytes already read), `Tail` (bytes not yet read), and non-advancing `PeekByte()`/`PeekBytes(n)` round out the API. There is **no** `ReadStringUtf8(n)` — use `ReadBytes(n).ToStringUtf8()`. ## 8. SliceOwner — pooled, disposable Slices `SliceOwner` is a rented `Slice` that returns its buffer to an `ArrayPool<byte>` on `Dispose` — the allocation-free analogue of `IMemoryOwner<byte>`. The contract: **you MUST `Dispose` it, and MUST NOT use its data afterward.** ```csharp using (var owner = Slice.FromBytes(payload, ArrayPool<byte>.Shared)) { Slice data = owner.Data; // valid only inside the using Use(data.Span); } // buffer returned to the pool here ``` `owner.IsValid`, `owner.Count`, `owner.Span`, `owner.Pool`; `SliceOwner.Wrap/Create/Copy` and `writer.ToSliceOwner()` produce them. Don't let an owner's `Data` escape the `using`. ## 9. Span / Memory interop & `ISpanEncodable` `Slice` interops freely with the modern primitives: `slice.Span` (`ReadOnlySpan<byte>`), `slice.Memory` (`ReadOnlyMemory<byte>`), `byte[].AsSlice()`. Many hot types (keys, values, the writers) implement **`ISpanEncodable`** so they can be rendered into a caller's buffer with no intermediate `Slice` allocation — `TryGetSpan(out span)` / `TryGetSizeHint(out size)` / `TryEncode(dest, out written)`. That interface is how `subspace.Key(...)`/`FdbValue.*` write themselves into pooled buffers at the last moment. For working directly over `Span<byte>` (a caller-owned, fixed buffer) instead of `Slice`, use `SpanReader`/`SpanWriter` — see [`references/span-readers-writers.md`](references/span-readers-writers.md). ## 10. Round-trip example ```csharp // build var w = new SliceWriter(); w.WriteInt32(order.Id); w.WriteVarString(order.Customer); w.WriteVarInt64(order.Total); Slice packed = w.ToSlice(); // parse var r = packed.ToSliceReader(); int id = r.ReadInt32(); string cust = r.ReadVarString(); long total = (long) r.ReadVarInt64(); ``` ## 11. Self-check - [ ] Did I use `IsNull`/`IsNullOrEmpty` (not `== Slice.Empty`) to test for a missing value? - [ ] Am I treating `Slice` as a **view** — copying with `ToArray()`/`ToSliceOwner()` before mutating shared arrays or outliving a pooled buffer? - [ ] Did I pick the right integer encoding (`Fixed*`/`*BE` for sortable keys; `VarInt*`/`Fixed*` for self-delimiting stream fields; `FromInt32` only for standalone whole-slice values)? - [ ] Do my `SliceWriter` writes and `SliceReader` reads pair up (`WriteInt32`↔`ReadInt32`, `VarInt`↔`VarInt`, `VarString`↔`VarString`)? - [ ] If I rented from an `ArrayPool` (`SliceWriter(pool)` / `SliceOwner`), did I `Dispose`/`ToSliceOwner()` so the buffer returns to the pool — and not use the data after disposal?
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.