Skip to content

BufferReaderWriter

TL;DR: WasmScripting.BufferReaderWriter is a ref struct that packs/unpacks primitives, arrays, strings, and spans into a byte buffer. Use it to build compact network payloads and file-storage blobs without touching JSON or reflection. Reads and writes advance the internal cursor; the internal buffer grows on write.

public ref partial struct BufferReaderWriter
{
public Span<byte> Buffer { get; }
public int Length { get; }
public int Position { get; set; }
public BufferReaderWriter(int initialCapacity = 64);
public BufferReaderWriter(byte[] data);
public BufferReaderWriter(Span<byte> data);
// Writers
public void Write<T>(T value) where T : unmanaged;
public void Write<T>(T[] value, bool writeLength = true) where T : unmanaged;
public void Write<T>(ReadOnlySpan<T> value, bool writeLength = true) where T : unmanaged;
public void Write(string value, Encoding encoding = null, bool writeLength = true);
// Readers (not shown — symmetric family: Read<T>(out T), ReadArray<T>, ReadString, ...)
}

Source: CVR.CCK.Wasm/Scripting/Links/APIs/BufferReaderWriter.cs.

  • Networking payloads — the Networking.SendMessage(Span<byte>, ...) signature takes a raw byte span; fill it with a BufferReaderWriter.
  • File storage blobsFileStorage.WriteFile(string, Span<byte>) and FileStorage.ReadFile(string) -> CVRFile both work in raw bytes. Encode with BufferReaderWriter rather than JSON for size + determinism.
  • Dense state — compact custom types on a [WasmSerialized] class where you’d otherwise pay for per-field serialization.
using WasmScripting;
var w = new BufferReaderWriter(256);
w.Write(1); // int version
w.Write(player.GetNetworkId()); // short
w.Write(localPlayer.GetPosition()); // Vector3 (unmanaged)
w.Write("ready", writeLength: true); // UTF-8 string with length prefix
int[] checkpointOrder = { 0, 1, 2, 3 };
w.Write(checkpointOrder); // array with length prefix
Networking.SendMessage(w.Buffer.Slice(0, w.Length), playerIds: null, SendType.Reliable);

The two-arg Networking.SendMessage(BufferReaderWriter, ...) overload is equivalent — it just calls Slice(0, Length) internally.

private void OnNetMessage(Player sender, Span<byte> bytes)
{
var r = new BufferReaderWriter(bytes);
r.Read(out int version);
if (version != 1) return;
r.Read(out short netId);
r.Read(out Vector3 pos);
r.Read(out string status, /*readLength:*/ true);
r.Read(out int[] order);
ApplyRemoteState(sender, pos, status, order);
}

By default string, array, and span writes prepend a 4-byte length so the reader knows how many elements to consume. Set writeLength: false when you can derive the length from context (fixed-size protocol field, rest-of-buffer slurp, etc.).

w.Write(payload, writeLength: false); // writer side
// reader side — caller tracks length externally
r.Read(out byte[] rest, bytes.Length - r.Position);

Use matching writeLength / readLength semantics on both sides.

The default for strings is UTF-8. Override by passing an Encoding instance:

w.Write(unicodeString, Encoding.Unicode);

The writer-mode constructor (new BufferReaderWriter(int initialCapacity)) allocates a buffer and grows it (roughly doubling) as needed. The reader-mode constructors ((byte[]), (Span<byte>)) reuse the provided storage; writes into a reader-mode buffer that exceed capacity throw.

For networking, size your initial capacity to the typical message to avoid resizes:

var w = new BufferReaderWriter(64); // tight enough for one position update

Because BufferReaderWriter is a ref struct:

  • Cannot be stored on the heap (no fields, no async capture, no boxing).
  • Cannot cross await / yield return.
  • Must live entirely on the stack within a single call.

Fine for the typical flow (build in one method, send, discard) but incompatible with long-running state you’d park between events.

Every Write<T> / Read<T> runs entirely inside the guest WASM module — no host boundary crossing. This is the cheapest possible serialization path in the sandbox; prefer it over string-based formats (JSON, XML) on hot paths.