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
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
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) */
| Directory | Responsibility | Criterion |
|---|---|---|
modules/ | Business-agnostic reusable capabilities | Could be copied into another app as-is |
route_http/ | Business handlers | Includes modules; nothing includes it |
page/ template/ | Dynamic content materials | Must be read through scripts, never URL-reachable |
wwwroot/ | Static assets | The only URL-reachable directory |
db/ | Persistent data | Untouched 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:
/* 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:
/* 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:
/* 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
- 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.
/* modules/a.h */ static void Init(void);
/* modules/b.h */ static void Init(void);
/* main.c includes both → compile fails */static void A_Init(void);
static void B_Init(void); /* module prefixes */- 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.
static uint64 G_iTotal; /* total requests
* after a hot reload = 0, "data lost" *//* export via ServiceSwap → take back with xsSwapTake
* (full implementation in chapter 28) */- 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).
/* put a download page at page/download.html
* and expect /download.html to work *//* public assets → wwwroot/
* controlled assets → page/ + LoadPage in a route */Exercises
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)?
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.
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).
Summary
| Point | Conclusion |
|---|---|
| Directory = responsibility | modules (reuse) / route_http (business) / page·template (controlled) / wwwroot (public) / db (cross-generation) |
| Single translation unit | main.c includes everything in dependency order; TCC compile cost is contained |
| Generation isolation | Statics belong to the generation; surviving a swap requires ServiceSwap |
| Module traits | Depend only downward, ownership comments, prefixed static symbols |
| Access boundary | Only wwwroot is URL-reachable; everything else goes through the script |
- xrt reference · template — the full API of the template engine behind template/
- xrt reference · map — the data structure under modules/protocol.h's route table