Glyph11.Native
The same hardened parser as Glyph11, running in the C core
(libglyph11) via P/Invoke. Native speed, zero allocation — you provide the field
storage and the parser writes byte-offset spans into it. Native binaries are bundled for six RIDs.
Install
dotnet add package Glyph11.Native
No native setup — the right libglyph11 is bundled per platform and resolved automatically.
Quick start
using System.Text;
using Glyph11.Native;
byte[] request = Encoding.ASCII.GetBytes(
"GET /api/users?page=1 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n");
// Your security policy. Default caps MaxHeaderCount at 100 (see "Storage & limits").
var limits = Glyph11Limits.Default;
// Caller-provided storage the parser fills with (offset,length) spans — no allocation.
// Size to the limits so any request the policy accepts fits (see the note below).
Span<Glyph11Field> headers = stackalloc Glyph11Field[(int)limits.MaxHeaderCount];
Span<Glyph11Field> query = stackalloc Glyph11Field[(int)limits.MaxQueryParameterCount];
// Returns a status int; `r` holds the request-line spans + counts.
int status = Glyph11Parser.Parse(request, headers, query, limits, out Glyph11Result r);
if (status == Glyph11Parser.Ok) // 0 = OK
{
// Fields are offsets into `request` — slice it to read them (see "Reading fields").
Console.WriteLine(Encoding.ASCII.GetString(request, (int)r.Path.Offset, (int)r.Path.Length)); // /api/users
}
Storage & limits
Glyph11Limits mirrors the C policy (all uint): MaxHeaderCount,
MaxHeaderNameLength, MaxHeaderValueLength, MaxUrlLength,
MaxQueryParameterCount, MaxMethodLength, MaxTotalHeaderBytes.
// Derive a custom policy (it's a struct — copy and set fields):
var limits = Glyph11Limits.Default;
limits.MaxHeaderCount = 64;
limits.MaxTotalHeaderBytes = 16 * 1024;
headers array smaller than MaxHeaderCount silently lowers your
effective limit: once it fills, parsing stops with TOO_MANY_HEADERS. For large limits, rent
from ArrayPool<Glyph11Field> instead of stackalloc.// Large limits → pool instead of stackalloc:
Glyph11Field[] headers = ArrayPool<Glyph11Field>.Shared.Rent((int)limits.MaxHeaderCount);
try
{
int status = Glyph11Parser.Parse(request, headers, query, limits, out var r);
// ...
}
finally { ArrayPool<Glyph11Field>.Shared.Return(headers); }
Reading fields
The output is offset-based: Glyph11Result and each Glyph11Field hold
Glyph11Span values (Offset + Length, uint) that index
into the buffer you passed. Nothing is copied; slice the buffer to read.
// A tiny helper to turn a span into a string against the input buffer.
string Slice(Glyph11Span s) => Encoding.ASCII.GetString(request, (int)s.Offset, (int)s.Length);
// --- request line (Glyph11Result) ---
Slice(r.Method); // GET
Slice(r.Target); // /api/users?page=1 (full request target, as received)
Slice(r.Path); // /api/users (query stripped)
Slice(r.Version); // HTTP/1.1
long bodyOffset = r.Consumed; // where the body begins (bytes consumed by the header block)
// --- headers: read r.HeaderCount entries from the array you passed ---
for (int i = 0; i < r.HeaderCount; i++)
Console.WriteLine($"{Slice(headers[i].Name)}: {Slice(headers[i].Value)}");
// --- query parameters: r.QueryCount entries ---
for (int i = 0; i < r.QueryCount; i++)
Console.WriteLine($"{Slice(query[i].Name)} = {Slice(query[i].Value)}");
ReadOnlySequence input
The C core needs one contiguous buffer, so a fragmented ReadOnlySequence<byte> uses a
dedicated overload: single-segment is parsed in place (zero-copy); multi-segment is linearized into a
scratch buffer you provide (keeping the package's zero-allocation contract). The out
parsed span tells you which buffer the offsets index into.
// `scratch` only needs to hold a request when the input is fragmented; size it to your
// MaxTotalHeaderBytes. Single-segment input leaves it untouched.
Span<byte> scratch = stackalloc byte[16 * 1024];
int status = Glyph11Parser.Parse(
sequence, scratch, headers, query, limits,
out Glyph11Result r,
out ReadOnlySpan<byte> parsed); // ← the contiguous bytes the offsets index into
if (status == Glyph11Parser.Ok)
{
// Slice against `parsed` (NOT the original sequence): it is either the input's first
// segment (single-segment) or `scratch` (multi-segment).
var method = parsed.Slice((int)r.Method.Offset, (int)r.Method.Length);
var path = parsed.Slice((int)r.Path.Offset, (int)r.Path.Length);
}
// Don't overwrite `scratch` while still reading fields from a multi-segment parse.
Status codes
int status = Glyph11Parser.Parse(request, headers, query, limits, out var r);
if (status == Glyph11Parser.Ok) { /* 0 — valid request parsed */ }
else if (status == Glyph11Parser.Incomplete) { /* 1 — need more bytes, read & retry */ }
else
{
// Any other value is a protocol/limit error. Map it to an HTTP response code:
int http = Glyph11Parser.HttpCode(status); // 400 (malformed) or 431 (limit exceeded)
}
// Sanity-check the loaded native library at startup if you like:
uint abi = Glyph11Parser.AbiVersion(); // packed major.minor.patch of libglyph11
Chunked decoding
The C core's streaming chunked decoder strips the framing and writes the decoded body into an output buffer you own. Feed it each network read — a chunk's payload may span calls; the decoder carries the partial state.
// One decoder per body. Init zeroes it (the struct is the whole state — no allocation).
Glyph11Chunked.Init(out Glyph11ChunkDecoder decoder);
Span<byte> output = stackalloc byte[64 * 1024]; // your drain buffer
Glyph11ChunkResult r = Glyph11Chunked.Decode(
ref decoder,
input, // a chunk-encoded network read
output, // decoded body bytes are written here
out int inConsumed, // input bytes consumed this call
out int outWritten); // decoded bytes written to `output`
switch (r)
{
case Glyph11ChunkResult.Ok: break; // input exhausted or output full — advance, drain, call again
case Glyph11ChunkResult.Done: break; // terminal chunk seen — body complete
case Glyph11ChunkResult.Error: break; // malformed framing — reject the request
}
Platforms & native resolution
Bundled native binaries (resolved automatically from runtimes/<rid>/native/):
| OS | RIDs |
|---|---|
| Linux | linux-x64, linux-arm64 |
| Windows | win-x64, win-arm64 |
| macOS | osx-x64, osx-arm64 |
linux-x64 requires AVX2 (Haswell / 2013+ — universal on modern
servers and cloud): the SIMD scanners inline into the parse loop for ~15% on large headers. Other RIDs
use the portable baseline.To point at a hand-built core (tests, local builds), set GLYPH11_NATIVE_PATH to an explicit
path to libglyph11.{so,dll,dylib}; otherwise the bundled binary is used.