Zero-Copy JSON and XML Stream Parsing in C

Zero-copy stream parsing in C enables high-throughput processing of large JSON or XML datasets by eliminating redundant heap allocations and memory-copy operations. Instead of duplicating strings and building in-memory abstract syntax trees (ASTs), a zero-copy stream parser reads data into contiguous memory chunks and returns pointers directly referencing slices within the input buffer. This article details the core architectural techniques required to implement an efficient, zero-copy, event-driven parser in C, including ring buffer management, handling tokens that cross buffer boundaries, in-place string mutation, and state machine design.

Architectural Fundamentals: Reference Slices vs. Allocations

Traditional parsers allocate separate memory blocks for every parsed string, key, and value using functions like malloc() or strdup(). Zero-copy parsers avoid this overhead by representing text segments via a lightweight slice structure containing a pointer to the original buffer and a length counter:

typedef struct {
    const char *data;
    size_t length;
} StringSlice;

When parsing an XML attribute or a JSON string value, the engine points data directly to the start byte inside the input stream. This approach reduces memory bandwidth consumption, keeps the CPU cache warm, and completely removes allocator contention.

Managing Stream Boundaries with Sliding Windows

Streaming network sockets or disk files deliver data in discrete chunks. The primary challenge in zero-copy stream parsing is handling tokens that are split across read boundaries (e.g., a long JSON string or an XML tag split between two consecutive read() calls).

To maintain zero-copy semantics across chunks, use a sliding window or a circular ring buffer:

  1. Active Region Maintenance: Read incoming chunks into a pre-allocated buffer.
  2. Shift on Boundary Split: When a token begins near the end of the buffer and reaches the end of the chunk without terminating, copy only the incomplete fragment to the start of the buffer.
  3. Refill and Continue: Read the subsequent chunk immediately following the preserved fragment. This ensures that the token becomes contiguous in memory, allowing client code to reference it using a single pointer and length without allocating intermediate heap storage.

Designing the Push vs. Pull State Machine

Zero-copy parsers generally avoid Document Object Model (DOM) representations in favor of streaming interfaces:

  • Push Parsers (SAX-style): The parser drives the execution loop, emitting callbacks with StringSlice parameters whenever tokens (such as element starts, keys, or text nodes) are encountered.
  • Pull Parsers (StAX-style): The consumer calls a next_token() function that drives a non-blocking Finite State Machine (FSM).

A pull-based state machine is often preferred in C because it avoids function call overhead and integrates naturally into asynchronous event loops (e.g., epoll or io_uring). The state machine tracks structural nesting using a shallow stack of enumeration tags rather than storing full object trees.

In-Place String Mutation and Unescaping

JSON and XML frequently encode escaped characters, such as \n, \", \uXXXX in JSON or &, < in XML. Transforming these sequences into their raw byte representations typically requires creating a new string.

Because an unescaped string is guaranteed to be equal to or shorter than its escaped representation, you can perform unescaping in-place directly within the read buffer:

  • Maintain two pointers: a read pointer scanning the escaped characters and a write pointer updating the buffer in-place.
  • Once unescaped, terminate or bound the slice at the final write position.
  • Note that in-place mutation alters the underlying buffer. If the raw input must be retained untouched, store both the original slice and an auxiliary decoded slice only when escape sequences are detected.

Leveraging SIMD for Structural Indexing

State-of-the-art parsers maximize throughput by using SIMD (Single Instruction, Multiple Data) vector intrinsics (such as AVX2, AVX-512, or ARM NEON) to scan input buffers for structural characters in 32- or 64-byte blocks.

  • In JSON, SIMD masks can locate quotes ("), colons (:), commas (,), and braces ({}, []) simultaneously.
  • In XML, SIMD searches can rapidly identify opening brackets (<), closing brackets (>), and whitespace.

By processing structural delimiters in parallel, the parser skips bulk payload text at memory-bus speeds and invokes the state machine only when a structural transition occurs.

Pointer Lifetimes and Memory Safety

Because string slices point directly to internal I/O buffers, consumer code must process or consume each token before the next read() operation overwrites the underlying memory. If a consumer needs to retain a value beyond the lifetime of the current buffer window, it becomes the consumer's explicit responsibility to allocate memory and copy the data. Documenting and strictly enforcing these buffer lifetimes guarantees high performance without risking dangling pointer dereferences.