Anatomy of a minimal app: two files make a service
A XServer app that runs, answers requests and falls back to static files needs just two files: xs.json and main.c. This chapter dissects them line by line — the foundation under all your future code.
Overview
4.1 Intro
Most frameworks' "hello world" crosses five layers of abstraction: entry registration → route matching → middleware chain → context object → response serialization. XServer's philosophy is the opposite: hand you the protocol's minimal facts first — the parsed request head, transport pointers, the framing state machine — then step aside. So the minimal app is genuinely minimal:
Now the line-by-line dissection of these two facts. They are taken from demo-single's real files, with the business parts trimmed away.
4.2 xs.json line by line
{
"services": [
{
"enabled": true, /* service switch */
"class": "http", /* protocol class: http/ws/tcp/udp/custom */
"name": "demo", /* globally unique name, the key for xsServerFind */
"ip": "127.0.0.1",
"port": 9080,
"host_default": { /* default host: fallback when no vhost matches */
"enabled": true,
"name": "app",
"path": "wwwroot", /* static asset root */
"devlang": "c", /* c = TCC script; static = pure static */
"devfile": "main.c" /* script entry (relative to the config directory) */
}
}
]
}
What just happened: this config declares a chain of facts — listen for HTTP on 127.0.0.1:9080; requests matching no other vhost go to the default host app; app's static root is wwwroot; the business logic is main.c. Any wrong field (a typo in class, an out-of-range port) fails startup before config loaded, naming the exact field — the full field table is in the guide · configuration, and volume 2 covers it field by field.
4.3 main.c line by line
#include <xsbase.h> /* contract header: pulls in xrt_decl.h and libtcc.h (all in the VFS) */
/* called once after this generation loads (assembly / reload control thread) */
static void ServiceInit(XS_HostInfo* pHost)
{
printf("[%s] service init\n", pHost->Name);
}
/* once per request (engine worker threads, may run concurrently) */
static XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
/* unhandled requests → fall back to the static layer (wwwroot), else 404 */
return XS_FALLBACK;
}
/* called when this generation retires (GC thread, at most once per lifetime) */
static void ServiceUnit(XS_HostInfo* pHost)
{
printf("[%s] service unit\n", pHost->Name);
}
What just happened:
#include <xsbase.h>needs no installation — the header, together with the full xrt declarations, lives in the exe's VFS and TCC reads it directly;- the host looks up
ServiceInit / RequestProc / ServiceUnitby symbol name (static doesn't hide them — symbols are visible in TCC's memory mode); all are optional; - the three functions map exactly onto the generation's three moments from chapter 3: birth / service / retirement.
Run it (output from a real machine):
$ ./xs [xs] config loaded: .../xs.json (1 servers) [xs] script loaded: .../main.c (0.4 KB gen 1) [app] service init [xs] server 'demo' http bound on 127.0.0.1:9080 [xs] running, press Ctrl+C to stop $ curl -s http://127.0.0.1:9080/ <!DOCTYPE html> ... /* wwwroot/index.html — the static layer took over / */
4.4 The three-way return
RequestProc's return value is not "success / failure" but who owns the rest of this request's life:
| Return | Meaning | What the driver does next |
|---|---|---|
XS_OK | I have written the complete response | Drain the remaining body, continue the keep-alive loop |
XS_FALLBACK | I didn't handle it | GET/HEAD fall to the static layer; other methods 404 |
XS_TAKEOVER | I'm taking over the whole connection | The driver steps out; the app sends/receives and eventually Closes (chapter 25) |
4.5 Static-layer fallback
After XS_FALLBACK the host's static layer takes over: it looks for files under the host's path (wwwroot here) — GET returns content, HEAD headers only — with percent-decoding, directory indexes (index.html by default) and dot-files denied by default. That is how / got index.html above. The static layer's four knobs (custom headers, error pages, directory indexes, deny_dotfiles) open up in volume 2, chapter 10.
This "script first, static fallback" combination means you write no code for static assets — and no rewrite rules for dynamic routes either.
4.6 Contract
- Symbols: contract symbols are looked up by name, all optional — a host that exports no RequestProc is simply a pure static site.
- Ownership:
XS_HttpReqand its inner pointers are valid only within the callback stack; a bare return is risk-free here, but once you start using views, chapter 3's rules apply. - Response: returning XS_OK means "the complete response has been written" — head + body (except HEAD) are your responsibility.
Common mistakes
- Symptom
- Some requests "randomly" misbehave: empty responses, hung or dropped connections.
- Cause
- A non-void function falling off the end is undefined behavior in C — the return value is a random stack leftover, and the three-way result is interpreted at random.
static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
if ( path is mine ) ...;
/* missing return: leftover randomly hits a state */
}static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
if ( path is mine ) return XS_OK;
return XS_FALLBACK; /* explicit fallthrough */
}- Symptom
- Clients fail to parse; on keep-alive the following requests garble.
- Cause
- Concatenating a string onto
pReq->tcpas a response — Content-Length, HEAD semantics and the connection state machine are all left unmaintained.
xrtNetStreamSend(pReq->tcp,
"HTTP/1.1 200 OK\n\nhello", 22);
return XS_OK; /* incomplete message, no length *//* build the head with xrtHttp1ResponseWrite
* (chapter 15 wraps it fully) */- Symptom
- File content mixes into the response body; browsers report decoding errors.
- Cause
- XS_FALLBACK lets the static layer keep writing the same connection; your half-written bytes became the message prefix.
ConnSend(pReq, head, n);
if ( !hasBody ) return XS_FALLBACK;/* decide before writing:
* either a complete response + XS_OK,
* or write nothing + FALLBACK */Exercises
Without looking at this chapter, create a directory and write xs.json + main.c; get it running and verify with curl that / returns the static page and /nope returns 404.
service init and gen 1; both curl checks behave as expected.Hints: use the C standard library's time() and strftime() (available in the TCC environment); for the response, an extremely simple head with a correct Content-Length will do for now — chapter 15 formalizes it.
curl -s http://127.0.0.1:9080/time returns the current time and curl parses the message cleanly (no trailing garbage).Return the POST body sent to /echo verbatim. Hints: read pReq->body (the fixed-length body API is xrt's xrtHttp1BodyRead); small bodies first is fine if full streaming is too much.
curl -d "hello xs" .../echo returns hello xs verbatim; test with 50 bytes and 5000 bytes.Summary
| Point | Conclusion |
|---|---|
| Minimal app | xs.json (assembly fact) + main.c (business fact) — nothing more |
| Contract symbols | Looked up by name, all optional; Init/Request/Unit map to the generation's three moments |
| Three-way return | XS_OK responded / FALLBACK to static / TAKEOVER the connection |
| Static layer | Takes over GET/HEAD after FALLBACK; four knobs in volume 2 |
| Headers | xsbase.h and all declarations live in the exe's VFS — zero install |
- xrt reference · HTTP/1.1 — full signatures of ResponseWrite / BodyRead
- xrt book · HTTP server — how the library assembles HTTP (compare with what the XServer host does for you)