Mental model: generations, leases and unwrapped pass-through
The most important chapter of the book. Every XServer behavior — why hot reload is safe, why old connections never drop, what you must not do in callbacks — can be derived from the three concepts in this chapter.
Overview
gen 2.
3.1 Intro: three questions
People new to "compile C at runtime" usually ask three questions that go straight to the heart:
- When does my code become machine code? — At startup and at every hot reload, compiled to native code by the embedded TCC; afterwards it executes exactly as fast as your precompiled C (routebench measures it on par with Go).
- Who owns connections, listeners, timers? — The host owns them. Scripts only "borrow" them inside callbacks.
- At the instant code changes, what about in-flight requests? — Two generations coexist; old requests are served to completion by the old code. That is this chapter's protagonist.
The script loaded ... gen 1 line from chapter 2's log already revealed the entrance: the generation. Now let's pin it down.
3.2 Three concepts
Concept 1: the generation
Every completed script load — startup or hot reload — creates a new generation. One generation = one complete script runtime: the compiled artifact, the static variables, everything ServiceInit built. So seeing gen 2 after a restart is no mystery: generation numbers increase monotonically within the process, and a restart is merely "a swap with no old generation present".
Concept 2: the lease
Every request / connection / timer that enters processing takes a lease (a reference) on "the generation it started working in". The lease guarantees: as long as work remains unfinished on the old generation, its code and data are never freed. That is the mechanical essence of "editing code without dropping connections" — not a deferred-destruction trick, but a reference-counting semantic guarantee.
Concept 3: reclaim at zero references
Once an old generation's references hit zero, a dedicated GC thread destructs it in a fixed order: ServiceUnit (script cleanup) → TCC state → driver runtime → config tree. The XServer test suite includes a dedicated three-cycle 8080↔8081 swap soak: every old generation reclaimed, zero leaks, zero crashes. You never write GC code — but you need to know it exists to understand why chapter 28's state handover is designed the way it is.
3.3 Division of labor & pass-through
What the host owns vs what the script owns
| Host (XServer) | Script (your C) |
|---|---|
| Reads xs.json, assembles services and listeners | Business logic, route tables, data processing |
| Protocol parsing (HTTP/WS frames, TLS records) | Builds responses from parsed results |
| Lifecycle of connections and listeners, GC | Static state (isolated per generation), ServiceInit/Swap/Unit |
| Hot reload compile, publish, rollback | Exports contract symbols, submits reload intents |
Unwrapped pass-through
Most frameworks wrap connections and requests into framework objects, and you work inside their abstraction. XServer deliberately wraps nothing: the XS_HttpReq your callback receives is a view over native xrt pointers — raw transport pointers, the parsed request head, the framing state machine. You can call any of xrt's 3,030 APIs on them directly: no hidden layer, no adaptation loss.
| Traditional framework wrapping | XServer view pass-through |
|---|---|
| req.headers() accessor wrappers | pReq->head is the xrt-parsed struct itself |
| res.send() goes through the framework pipeline | xrtHttp1ResponseWrite + stream sends — you control every step |
| Object lifetime belongs to the framework | Views are valid only inside the callback; ownership rules are explicit |
"Zero wrapping" is freedom and responsibility: a view cannot be stored beyond its callback (see mistake ①), but no framework magic stands between you and the protocol either.
3.4 Example: seeing generations
Generations need no special tooling to observe — the boot log is itself the evidence. Revisit chapter 2's real output:
[xs] engine started, workers=12
[xs] script loaded: .../main.c (2.6 KB gen 1) /* ← generation 1 is born: 2.6KB of source compiled, numbered 1 */
[demo] database opened: .../db\main.db /* ← gen1's ServiceInit is running */
What just happened: TCC compiled 2.6KB of source into native code and created the "generation 1" script runtime; the [demo]-prefixed line then proves the app's own ServiceInit ran on that generation. When you edited route.h and restarted in chapter 2's exercise, the log showed gen 2 — a new generation born, the old process gone, nothing left to reclaim. Hot reload (volume 4) does the same thing while the old generation is still serving: after the new generation publishes, the old one waits for its last lease to be returned.
3.5 Contract
- Threading: script callbacks run on engine worker threads (workers=12 means callbacks may run concurrently); ServiceInit / ServiceSwap run on the assembly / reload control thread; ServiceUnit on the GC thread. Synchronizing cross-thread shared state is your job.
- Ownership: views received in callbacks (XS_HttpReq, head pointers, message views) are valid only for the callback stack; copy before storing. Resources (connections, listeners, timers) belong to the host — scripts merely borrow them.
- Lifecycle: a retired generation accepts no new requests; statics are isolated per generation — a swap means brand-new static state, and cross-generation transfer must go through ServiceSwap (chapter 28).
Common mistakes
- Symptom
- Sporadic crashes / corrupted data, especially under high concurrency or after a hot reload.
- Cause
- Views like
pReq->headare valid only within the callback stack; after the callback returns the buffers may be reused, and after a swap the old generation's memory is reclaimed.
static xhttp1head* G_Head;
XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
G_Head = pReq->head; /* read later outside → UAF *//* take the values you need inside the callback */
const char* sUA = ...; /* copy the string */
G_iCount++; /* values may escape, pointers may not */- Symptom
- Putting code from untrusted sources into devfile, or assuming a script crash / out-of-bounds access cannot hurt the host.
- Cause
- Scripts are same-process native code: they share the host's address space and privileges and can call all 3,030 APIs and system libraries. The absence of a sandbox is intentional design.
/* "if the script crashes the host survives" — false.
* a wild pointer pierces the whole process *//* devfile holds only code maintained by trusted ops;
* capabilities needing isolation go to a separate process */- Symptom
- Slow startup; new requests queue up during a hot reload.
- Cause
- ServiceInit runs serially on the assembly / reload control thread; the new generation must finish Init before publishing.
void ServiceInit(XS_HostInfo* pHost)
{
BigIndexLoad(); /* blocks for 8 seconds *//* light init + lazy load on first request,
* or build on a background thread and publish a ready flag */Exercises
Run the demo and mark in the log: which line announces a generation's birth? Which line proves ServiceInit ran on it? What is the worker count, and what does it imply about callback concurrency?
Requirement: count active connections and expose the number at /stats. Write down: where does the counting static live? When to increment and decrement (hint: WsOpen/WsClose or EventOpen/EventClose)? How does the count survive a hot reload (hint: what does ServiceSwap export)?
With the book closed, draw the complete timeline from "new generation compiling" to "old generation destructed" while 10 concurrent requests are in flight; mark which generation serves each request, when each lease is returned, and when GC acts.
Summary
| Point | Conclusion |
|---|---|
| Generation | Every script load creates one; statics are isolated per generation |
| Lease | Requests / connections / timers hold a reference to their generation, protecting it from release |
| Reclaim at zero | The GC thread destructs in order: ServiceUnit → TCC → driver → config |
| Division of labor | Host: assembly / parsing / lifecycle; script: business and state |
| Unwrapped pass-through | Callbacks receive native xrt views; values may leave the callback, pointers may not |
- xrt book · memory & ownership — the reference-counting infrastructure behind leases
- xrt book · concurrency volume intro — the concurrency model behind workers=12