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

Organizing the app directory: one translation unit, per-generation isolation

C projects are free to organize directories without limit. This chapter presents the demo-single proven conventions — not a mandatory standard, but the one that meshes best with hot reload's generation-isolation semantics.

Overview

Positioning: answers "where does code go". After this chapter every file you write has a clear home; volume 3's protocol chapters reuse these conventions directly. Readers with large-C-project experience should focus on 5.3's "why a single translation unit is safe".

5.1 Intro

The first question a new XServer app faces is not "how do I write a handler" but "how do I arrange files". C has no package manager drawing boundaries for you: include paths, symbol visibility, compilation-unit splits are all self-discipline. Messy organization gets amplified under hot reload — what gets reloaded is the entire translation unit, and every file's position affects every generation's compile.

demo-single offers a light convention: directories are responsibilities, one translation unit, self-contained modules. It supports a complete app with 19 routes, SQLite, templates and controlled pages, and survived routebench's ten-thousand-route stress — proof that the convention scales.

5.2 Directory conventions

demo-single directory tree verified on a real machine source: release/demo-single/
myapp/
├── xs.exe          /* the only binary (the host) */
├── xs.json         /* assembly facts: services/ports/TLS/vhosts */
├── main.c          /* app entry: include orchestration + lifecycle */
├── route.h         /* route manifest: URI × method → handler */
├── includes/       /* dev_inc: extra script header search path */
├── librarys/       /* dev_lib: third-party libs (#pragma comment) */
├── modules/        /* reusable modules: http / db / template / resource / protocol */
├── route_http/     /* request handlers: one file per resource */
├── page/           /* controlled pages: outside wwwroot, no direct links */
├── template/       /* xrtTemplate templates */
├── db/             /* SQLite data files */
└── wwwroot/        /* static asset root (the only URL-reachable directory) */
DirectoryResponsibilityCriterion
modules/Business-agnostic reusable capabilitiesCould be copied into another app as-is
route_http/Business handlersIncludes modules; nothing includes it
page/ template/Dynamic content materialsMust be read through scripts, never URL-reachable
wwwroot/Static assetsThe only URL-reachable directory
db/Persistent dataUntouched by reloads, shared across generations

Note the contrast in the last two rows: wwwroot is "URL-reachable", db is "cross-generation" — the conventions encode an access boundary and a lifetime boundary at once.

5.3 Single-translation-unit orchestration

The whole app merges into one translation unit: main.c includes everything in dependency order. The demo's orchestration:

main.c include orchestration (excerpt) source: release/demo-single/main.c
/* 1. SDK contract */
#include <xsbase.h>
/* 2. reusable modules (bottom-up) */
#include "modules/resource.h"
#include "modules/http.h"
#include "modules/database.h"
#include "modules/template.h"
#include "modules/protocol.h"  /* the routing framework: last, depends on all above */
/* 3. business handlers */
#include "route_http/basic.h"
#include "route_http/item.h"
/* 4. route manifest (references every handler) */
#include "route.h"

Why a single translation unit is safe

  • Compile cost is contained: TCC is fast; the demo's full 2.6KB entry compiles in one boot-log line, script loaded (2.6 KB gen N); routebench's ten-thousand-route app compiles just as instantly.
  • The simplest symbol world: no cross-.c link boundaries; visibility of static and global functions is obvious; no hidden trap of "two compilation units each defining the same-named global".
  • It meshes with generation isolation: a swap recompiles the entire unit; all statics update as a set — no half-new-half-old assembly.

Statics are isolated per generation

This is the most important semantic in these conventions. The demo's routing module source says it outright:

modules/protocol.h (comment, verbatim) source: release/demo-single/modules/protocol.h
/* A hot reload creates a fresh set of static variables;
 * the old generation runs ServiceUnit only after all requests leave. */

In other words: the route table in route.h, the G_DB handle in database.h — all of it is state of the generation. After a swap the new generation starts from a blank static area (ServiceInit rebuilds the route table, reopens the database), and the old set is destroyed as references hit zero. Anything you want to survive across generations (connection counts, caches) must transfer explicitly via ServiceSwap (chapter 28).

5.4 The modules reuse pattern

A good module satisfies "copy it into another app and it just works". Take ReplyJSON from modules/http.h as the boundary example:

modules/http.h · module boundary example source: release/demo-single/modules/http.h
/* ReplyJSON only borrows pObject; the caller still owns the xrtValueRelease. */
static bool ReplyJSON(XS_HttpReq* pReq, uint16 iStatus, const xvalue* pObject)
{
    /* serialize → ReplyRaw writes head and body → ownership spelled out in the comment */
    ...
}

What just happened — all three traits of a proper module in five lines:

  • Depends only downward: xsbase.h + xrt, nothing from route_http;
  • Ownership comments: the first line states "borrows, does not take over" — the caller never has to guess about freeing;
  • static functions: under one translation unit, static means "module-private" without polluting the app's namespace.

5.5 Contract

  • Dependency: include order = dependency order (SDK → modules → route_http → route.h); modules also stay one-directional among themselves.
  • Naming: under one translation unit every static symbol shares a namespace — module symbols carry a module prefix (e.g. RouteHTTP_, DB_, Template_).
  • Lifecycle: statics belong to the generation; file resources (db/, page/, template/) are shared across generations and are not rebuilt on a swap.

Common mistakes

Two modules define the same static function name
Symptom
Script compile fails outright: a redefinition error.
Cause
In one translation unit every .h expands into the same scope; same-named static functions inevitably clash — not a linker issue, it explodes at compile time.
✗ Wrong
/* modules/a.h */ static void Init(void);
/* modules/b.h */ static void Init(void);
/* main.c includes both → compile fails */
✓ Right
static void A_Init(void);
static void B_Init(void);  /* module prefixes */
Cross-generation data stored in a static
Symptom
Statistics zero out, caches vanish after a hot reload — "but I never restarted".
Cause
Statics are isolated per generation: a swap means a fresh set. Not a bug — a semantic. But if you expected continuity, you stepped on it.
✗ Wrong expectation
static uint64 G_iTotal;  /* total requests
 * after a hot reload = 0, "data lost" */
✓ Right
/* export via ServiceSwap → take back with xsSwapTake
 * (full implementation in chapter 28) */
A file meant to be public placed under page/
Symptom
Browsing straight to the file's path returns 404 — "the file is right there".
Cause
Only wwwroot is URL-reachable; page/ is a controlled directory that must be read through the script's LoadPage (chapter 19).
✗ Wrong
/* put a download page at page/download.html
 * and expect /download.html to work */
✓ Right
/* public assets → wwwroot/
 * controlled assets → page/ + LoadPage in a route */

Exercises

BasicDirectory checkup

Against 5.2's criteria, audit the app you built in chapter 4's exercise: does every file have a clear home? Any mixing (templates and static assets in one directory)?

Acceptance: a three-column table file → directory → criterion, with zero "mixed" rows.
AdvancedWrite a counter module

Create modules/counter.h providing Counter_Inc() / Counter_Get(), with a module prefix, standard-library-only dependencies and complete ownership comments. Call it from two different handlers.

Acceptance: copying counter.h into an empty directory (keeping only the xsbase.h include as its dependency) still compiles; both handlers see the same count.
ChallengeVerify generation isolation

Accumulate request counts with the counter module; confirm they reset after a restart; then read the demo's ServiceSwap declaration (XS_ServiceSwapProc in sdk/xsbase.h) and sketch your counter's cross-generation handover (pseudocode is fine).

Acceptance: you can state which values should transfer, which should reset, and who frees the object exported by Swap.

Summary

PointConclusion
Directory = responsibilitymodules (reuse) / route_http (business) / page·template (controlled) / wwwroot (public) / db (cross-generation)
Single translation unitmain.c includes everything in dependency order; TCC compile cost is contained
Generation isolationStatics belong to the generation; surviving a swap requires ServiceSwap
Module traitsDepend only downward, ownership comments, prefixed static symbols
Access boundaryOnly wwwroot is URL-reachable; everything else goes through the script
xrt further reading