Routing: static table + dynamic patterns + method slots
The host ships no router — by design, not omission. This chapter hands you the routing framework proven in demo-single and stress-tested by routebench at ten thousand routes: two tables, one compile, three returns.
Overview
17.1 Intro
"Should routing live in the host?" is a real question: built in, flexible path needs (regex, prefixes, wildcards) start squeezing the config system; left out, every app writes its own. XServer's choice is to provide parts, not the whole machine: xrtMap (a hash table) for exact matches, xrtPattern (a batch pre-compiled pattern matcher) for parameterized paths — twenty lines of assembly buy a production-grade router, and special needs (regex routing, version prefixes) can grow their own anytime.
17.2 The two-table pattern
static void RouteHTTP_Init(void)
{
G_StaticRouteTableHTTP = xrtMapCreate(sizeof(RouteInfoHTTP));
/* static table: exact URIs (xrtMap hash) */
AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_GET, Handle_API_List);
AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_POST, Handle_API_Add);
/* dynamic table: {id} segment capture (compiled after registration) */
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_GET, Handle_API_Item);
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_PUT | XHTTP_METHOD_PATCH, Handle_API_Item_Update);
AddDynamicRouteHTTP("/api/v1/items/{id}",
XHTTP_METHOD_DELETE, Handle_API_Item_Delete);
/* in-segment capture works too: /api/v1/file/{name}.txt */
AddDynamicRouteHTTP("/api/v1/file/{name}.txt",
XHTTP_METHOD_GET, Handle_API_File);
/* dynamic routes must be compiled once after all registration */
(void)RouteHTTP_Compile();
}
What just happened: the static table hits in O(1); the dynamic table is batch-compiled inside RouteHTTP_Compile via xrtPatternCompileMany into an immutable matcher — afterwards matching cost depends only on path length, never on route count. The handler signature is the demo convention: (XS_HttpReq*, const RouteParamHTTP* arrParam, uint32 iParamCount), with arrParam carrying the captured {id} views.
17.3 Method slots & 405
- Each (URI, method) pair is a slot — one URI with several methods registers separately; a duplicate URI+method overwrites the old callback with a warning
- Method masks combine freely:
PUT | PATCHshare a slot;XHTTP_METHOD_CRUD(five methods) andXHTTP_METHOD_ANY(incl. extensions) are aggregate macros (the enums reference) - URI hit but method miss → 405 + Allow header: Allow aggregates every method registered on that URI — the origin of chapter 2's
HEAD /json → 405, Allow: GET
GET | HEAD — your ReplyRaw already handles HEAD not sending a body uniformly (chapter 15), so most handlers support HEAD correctly with zero changes.17.4 Match order & performance
Performance numbers (routebench, 3 rounds × 10,000 requests averaged):
| Route count | XServer latency | Comparison |
|---|---|---|
| 10 | 42.0 µs | Go 42.2 |
| 10,000 | 59.1 µs (+41%) | PHP +492% / FastAPI +935% |
The secret of flatness: the static table is an O(1) hash; the dynamic table, after batch compilation, matches in O(path length) — ten thousand routes cost only 41% more than ten, while the comparison stacks degrade by an order of magnitude.
17.5 Contract
- Compile: dynamic routes must Compile once after registration; the table is immutable afterwards (rebuilding on swap is hot reload's job).
- Order: static → dynamic → FALLBACK; for the same URI, static beats dynamic.
- Params: arrParam is a per-request view — use it up inside the callback.
Common mistakes
- Symptom
- All dynamic routes 404 / fall into the static layer; static routes work fine.
- Cause
- The dynamic table becomes a matchable immutable pattern only after Compile — registration merely collects. The demo puts Compile at the end of RouteHTTP_Init with a "must" comment.
- Symptom
- Catch-alls like "/api/{a}/{b}/{c}" steal each other's traffic and hit the wrong handler.
- Cause
- Patterns match exact segments (not prefixes/regex); but stuffing many resources into one pattern drifts the semantics. Different resources get different patterns.
Exercises
Implement full REST for /api/v1/users and /api/v1/users/{id} (GET list / POST create / GET item / PUT+PATCH update / DELETE remove + HEAD list).
Watch several "URI hit, method miss" requests with -v; verify the Allow header matches your registrations exactly.
Script-generate 1000 static + 1000 dynamic routes (see tools/routebench's generator); measure compile time at startup and request latency.
Summary
| Point | Conclusion |
|---|---|
| Two tables | xrtMap static O(1) + xrtPattern dynamic, flat after compilation |
| Method slots | one slot per (URI × method); combinable masks; CRUD/ANY aggregates |
| 405 | URI hit, method miss → 405 + Allow (union of registered methods) |
| Order | static → dynamic → FALLBACK → static layer / 404 |
| Measured | 10k routes 59.1µs (+41%); comparison stacks degrade an order of magnitude |
- xrt reference · pattern — full CompileMany/Lookup signatures and capture views
- xrt reference · regex — RegexSet batch pre-compilation when you need regex routes