native · P/Invoke

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;
Size the arrays to your limits. The parser bounds-checks every write — it never overflows. But a 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)}");
Lifetime: the offsets are valid only while the input buffer is alive and unmodified. Read (or copy out) the fields you need before reusing the buffer for the next request.

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/):

OSRIDs
Linuxlinux-x64, linux-arm64
Windowswin-x64, win-arm64
macOSosx-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.