Chapter 3 / 51
Volume 1 · Getting Started & Mental Model

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

Positioning: no business code here — we install the "concept operating system". Done means: with the book closed you can explain to a colleague what happens to in-flight requests at the instant you edit code. This chapter also resolves the cliffhanger from chapter 2's exercise — why a restart shows 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".

Listener slot —— the stable entry on port 9080 │ the slot's pData always points at "the current generation" ▼ ┌──────────────┐ new connections enter │ current gen2 │ ◀───────────────── └──────────────┘ ▲ switch (atomic) ┌──────────────┐ existing connections continue │ old gen1 │ ◀───────────────── └──────────────┘ │ last connection leaves (references hit zero) ▼ GC thread: safely destruct script → driver → config

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.

The three concepts in one sentence: generations are the isolation unit, leases are the protection mechanism, zero references is the reclamation trigger. For the next 40 chapters, any lifecycle question — come back to this sentence first.

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 listenersBusiness logic, route tables, data processing
Protocol parsing (HTTP/WS frames, TLS records)Builds responses from parsed results
Lifecycle of connections and listeners, GCStatic state (isolated per generation), ServiceInit/Swap/Unit
Hot reload compile, publish, rollbackExports 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 wrappingXServer view pass-through
req.headers() accessor wrapperspReq->head is the xrt-parsed struct itself
res.send() goes through the framework pipelinexrtHttp1ResponseWrite + stream sends — you control every step
Object lifetime belongs to the frameworkViews 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:

Boot log (excerpt) verified on a real machine
[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

Storing a view pointer in a global
Symptom
Sporadic crashes / corrupted data, especially under high concurrency or after a hot reload.
Cause
Views like pReq->head are 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.
✗ Wrong
static xhttp1head* G_Head;
XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
    G_Head = pReq->head;  /* read later outside → UAF */
✓ Right
/* take the values you need inside the callback */
const char* sUA = ...;   /* copy the string */
G_iCount++;              /* values may escape, pointers may not */
Assuming the script has a sandbox
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.
✗ Wrong assumption
/* "if the script crashes the host survives" — false.
 * a wild pointer pierces the whole process */
✓ Right posture
/* devfile holds only code maintained by trusted ops;
 * capabilities needing isolation go to a separate process */
Long blocking inside ServiceInit
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.
✗ Wrong
void ServiceInit(XS_HostInfo* pHost)
{
    BigIndexLoad();   /* blocks for 8 seconds */
✓ Right
/* light init + lazy load on first request,
 * or build on a background thread and publish a ready flag */

Exercises

BasicPoint out the generation in the log

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?

Acceptance: all three questions answered by pointing at concrete log lines.
AdvancedDesign a connection counter

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)?

Acceptance: the design clearly separates "this generation's state" from "cross-generation handover" and never stores connection objects in a global.
ChallengeNarrate the full timeline

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.

Acceptance: the timeline never frees an old generation while a request is unfinished; you can point at where the timeline aborts when compilation fails.

Summary

PointConclusion
GenerationEvery script load creates one; statics are isolated per generation
LeaseRequests / connections / timers hold a reference to their generation, protecting it from release
Reclaim at zeroThe GC thread destructs in order: ServiceUnit → TCC → driver → config
Division of laborHost: assembly / parsing / lifecycle; script: business and state
Unwrapped pass-throughCallbacks receive native xrt views; values may leave the callback, pointers may not
API references in this chapter
xrt further reading