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

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

Positioning: shrink chapter 2's "running demo" to the smallest understandable unit. Afterwards you can write a blank XServer app without looking. If you've already written demo routes, focus on 4.4 (the three-way return) and mistake ① — the most frequent beginner trap.

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:

xs.json declares "listen where, script where" (assembly fact) main.c declares "what to do when a request arrives" (business fact) └──── two facts, one service ────┘

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

xs.json verified on a real machine source: release/demo-single/xs.json
{
    "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

main.c verified on a real machine minimized from demo-single/main.c
#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 / ServiceUnit by 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):

Terminal · real output
$ ./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:

ReturnMeaningWhat the driver does next
XS_OKI have written the complete responseDrain the remaining body, continue the keep-alive loop
XS_FALLBACKI didn't handle itGET/HEAD fall to the static layer; other methods 404
XS_TAKEOVERI'm taking over the whole connectionThe driver steps out; the app sends/receives and eventually Closes (chapter 25)
The three states are mutually exclusive and mandatory: before returning XS_OK you must truly have finished the response; once any bytes have been written to the connection you may no longer return XS_FALLBACK (the static layer would write a second copy and corrupt the message). The driver cross-checks the bytes your script declares consumed against what actually went on the wire; a mismatch drops the connection.

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_HttpReq and 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

Missing return (undefined behavior)
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.
✗ Wrong
static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
    if ( path is mine ) ...;
    /* missing return: leftover randomly hits a state */
}
✓ Right
static XS_RequestResult
RequestProc(XS_HttpReq* pReq)
{
    if ( path is mine ) return XS_OK;
    return XS_FALLBACK;   /* explicit fallthrough */
}
Hand-writing responses past the state machine
Symptom
Clients fail to parse; on keep-alive the following requests garble.
Cause
Concatenating a string onto pReq->tcp as a response — Content-Length, HEAD semantics and the connection state machine are all left unmaintained.
✗ Wrong
xrtNetStreamSend(pReq->tcp,
    "HTTP/1.1 200 OK\n\nhello", 22);
return XS_OK;   /* incomplete message, no length */
✓ Right
/* build the head with xrtHttp1ResponseWrite
 * (chapter 15 wraps it fully) */
Half a response, then FALLBACK
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.
✗ Wrong
ConnSend(pReq, head, n);
if ( !hasBody ) return XS_FALLBACK;
✓ Right
/* decide before writing:
 * either a complete response + XS_OK,
 * or write nothing + FALLBACK */

Exercises

BasicReproduce the minimal app from scratch

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.

Acceptance: the log contains service init and gen 1; both curl checks behave as expected.
AdvancedA /time endpoint

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.

Acceptance: curl -s http://127.0.0.1:9080/time returns the current time and curl parses the message cleanly (no trailing garbage).
ChallengeA minimal /echo

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.

Acceptance: curl -d "hello xs" .../echo returns hello xs verbatim; test with 50 bytes and 5000 bytes.

Summary

PointConclusion
Minimal appxs.json (assembly fact) + main.c (business fact) — nothing more
Contract symbolsLooked up by name, all optional; Init/Request/Unit map to the generation's three moments
Three-way returnXS_OK responded / FALLBACK to static / TAKEOVER the connection
Static layerTakes over GET/HEAD after FALLBACK; four knobs in volume 2
Headersxsbase.h and all declarations live in the exe's VFS — zero install
xrt further reading