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.
When to use
Section titled “When to use”- Networking payloads — the
Networking.SendMessage(Span<byte>, ...)signature takes a raw byte span; fill it with aBufferReaderWriter. - File storage blobs —
FileStorage.WriteFile(string, Span<byte>)andFileStorage.ReadFile(string) -> CVRFileboth work in raw bytes. Encode withBufferReaderWriterrather than JSON for size + determinism. - Dense state — compact custom types on a
[WasmSerialized]class where you’d otherwise pay for per-field serialization.
Writing
Section titled “Writing”using WasmScripting;
var w = new BufferReaderWriter(256);w.Write(1); // int versionw.Write(player.GetNetworkId()); // shortw.Write(localPlayer.GetPosition()); // Vector3 (unmanaged)w.Write("ready", writeLength: true); // UTF-8 string with length prefixint[] 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.
Reading
Section titled “Reading”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);}Length prefixes
Section titled “Length prefixes”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 externallyr.Read(out byte[] rest, bytes.Length - r.Position);Use matching writeLength / readLength semantics on both sides.
Encoding
Section titled “Encoding”The default for strings is UTF-8. Override by passing an Encoding instance:
w.Write(unicodeString, Encoding.Unicode);Capacity
Section titled “Capacity”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 updateref struct constraints
Section titled “ref struct constraints”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.
Performance
Section titled “Performance”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.
Related
Section titled “Related”- Networking — where outgoing buffers are sent.
- File Storage — where blobs land on disk.
- Serialization — higher-level Unity-inspector-style serialization (not this).