xs3 · xrt 2.0 · 契约头 xsbase.h

开发指南

从构建到热重载的完整路径。本页所有配置字段、契约签名与代码示例均与主线源码核对(config.h / xsbase.h / demo-single)。

快速开始

三个动作:构建宿主 → 写一份 xs.json 与 C 应用源码 → 运行。

1. 构建宿主

需要 GCC(可用 --cc 覆盖)与 Python 3。位置参数选择扩展变体,Windows / Linux 同一套流程:

build.bat                /* 最小变体 */
build.bat sqlite xtp     /* 按名挑选:SQLite 与 XTP2 */
build.bat all            /* 全部十个可选库一键编入 */
build.sh all             /* Linux 等价 */
xs --version             /* 版本与已编入扩展清单 */

链接成功才原子替换产物 —— 构建失败永远保留上一个可用的 xs。产物是单文件可执行程序,TCC 编译器与全套 SDK 头已内嵌其中。

2. 最小配置与应用

/* xs.json —— 取自 demo-single 的真实配置 */
{
    "services": [
        {
            "enabled": true,
            "class": "http",
            "name": "demo",
            "ip": "127.0.0.1",
            "port": 9080,
            "host_default": {
                "enabled": true,
                "name": "app",
                "path": "wwwroot",
                "devlang": "c",
                "devfile": "main.c"
            }
        }
    ]
}
/* main.c —— 最小可运行脚本 */
#include <xsbase.h>

static void ServiceInit(XS_HostInfo* pHost)
{
    printf("[%s] service init\n", pHost->Name);
}

static XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
    /* 未处理的请求落回静态层(wwwroot),其余 404 */
    return XS_FALLBACK;
}

3. 运行

./xs           /* 默认读取应用目录下的 xs.json */
./xs my.json   /* 指定配置文件 */

启动日志:[xs] config loaded → engine started → script loaded → 各端口 bound → [xs] running。浏览器访问 http://127.0.0.1:9080/ 即得到 wwwroot 下的静态页面。

应用结构

一个 XServer 应用 = 宿主 exe + 一个源码目录。目录约定由你自己定义,以下是 demo-single 验证过的组织方式:

myapp/
├── xs.exe          /* 唯一的二进制(5.2MB,含编译器与 SDK)*/
├── xs.json         /* 服务 · 端口 · TLS · vhost */
├── main.c          /* 应用入口:include 编排 + ServiceInit/Unit */
├── route.h         /* 路由注册清单 */
├── modules/        /* 复用模块(http/db/template/resource…)*/
├── route_http/     /* 请求处理器 */
├── page/           /* 受控页面(wwwroot 之外,防直链)*/
├── template/       /* xrtTemplate 模板 */
├── db/             /* SQLite 数据文件 */
└── wwwroot/        /* 静态资源根 */

全部源文件最终合成一个翻译单元(main.c include 一切),由内嵌 TCC 编译为原生机器码。静态变量按脚本代隔离:热重载会创建一套新的静态变量,旧代在全部请求离开后才执行 ServiceUnit —— 这正是热重载的语义基础。

脚本额外头文件放 dev_inc 目录、第三方库放 dev_lib 目录(xs.json 中声明),TCC 会自动搜索;#pragma comment(lib, ...) 可直接引用。

配置参考(xs.json)

配置装载 fail-fast:任何字段非法都在启动时给出具体字段名的错误并退出,不带病运行。

根级字段

字段类型 / 约束说明
engine.workersuint,0–1024引擎工作线程数,0 = xrt 默认。热重载不可变,变更会被拒绝
services数组,必填服务清单,缺失即启动失败

服务级字段(services[i])

字段类型 / 约束说明
enabledbool,默认 true是否启用
class白名单http / ws / tcp / udp / custom
name必填、全局唯一xsServerFind 的键
ip默认 0.0.0.0监听地址
port1–65535,非 custom 必填明文端口
tlsbool启用 TLS 端口(udp 上禁止)
port_tls默认 443与明文端口同 IP 冲突时拒绝启动
ip_tls缺省复用 ipTLS 监听地址
backloguint32直通 xrt 监听配置
recv_limit≤SIZE_MAX;udp ≤65535流读上限
path / devlang / devfile / dev_inc / dev_lib—服务级脚本缺省,合成进 DefaultHost

主机级字段(host_default / hosts[i])

字段类型 / 约束说明
enabledbool是否启用
nameserver 内唯一、非空主机名
host分号分隔域名http/ws 的启用 vhost 必填非空;路由按 Host 头 / SNI 匹配
path路径静态资源 / 脚本根
devlangstatic / c / 自定static = 纯静态;c = TCC 脚本
devfile路径脚本入口文件
tls_ca / tls_cert / tls_keyPEM 路径host 级证书 → SNI 选择器数据源,支持热替换
dev_inc / dev_lib分号分隔目录脚本额外 include / 库搜索路径(相对 appPath)

应用自读旋钮(Custom 字段)

不弹出预设结构、键保留给应用读取(经 server->Custom / xsConfigRoot())。宿主与 demo 惯例:

字段位置约束 / 默认
header_limitserver请求头上限
path_limitserver路径长度上限,默认 2048
body_limitserver请求体上限(超限 413)
idle_timeoutserver空闲超时(http/ws/tcp),到点服务端主动关闭
ws_message_limitserverWS 消息上限,超限以 close code 1009 关闭
ws_protocolserver强制要求的子协议(启动时校验合法性)
static.headershost≤8 字段,禁 8 种保留头(含 Transfer-Encoding)
static.indexhost目录索引数组,逐个回落(默认 index.html)
static.error_pageshost400–599 状态码 → 相对路径自定义错误页
static.deny_dotfileshost默认 true,点开头文件恒拒(含穿越)

所有文件路径按配置文件所在目录转绝对路径 —— 配置目录自包含,可整体搬移。

脚本契约

宿主按符号名在编译产物中查找回调,全部可选导出、按需声明。这就是 XServer 与应用的全部约定 —— 没有基类、没有注册宏。

符号适用协议签名(xsbase.h)
ServiceInit全部void (XS_HostInfo*) 装载后初始化
ServiceUnit全部void (XS_HostInfo*) 退役析构,全生命周期至多一次
ServiceSwap全部bool (XS_HostInfo*, xvalue**) 换代前导出状态快照
RequestProchttp/httpsXS_RequestResult (XS_HttpReq*) 请求入口
WsOpen / WsText / WsBinaryws/wss(XS_HostInfo*, xwsstream*[, 消息视图])
WsPing / WsPong / WsClosews/wss同上;Close 含 uint16 状态码与原因视图
EventOpen / EventData / EventClosetcp/tcps(XS_HostInfo*, XS_StreamConn*[, xnetbuf*/结果])
EventDgramudp(XS_HostInfo*, xnetudp*, const xnetudpmessage*)

RequestProc 三态返回

返回值语义
XS_OK已写出完整响应,驱动消费 body 余量后继续 keep-alive
XS_FALLBACK未处理:GET/HEAD 落入静态层,其余 404
XS_TAKEOVER应用接管连接:用 pull/Future 自行收发并最终 Close;Close 终态由 XServer 释放 lease
信任模型:脚本是编译后的原生代码,与宿主同进程同权限(无沙箱)。devfile 必须来自可信运维 —— 这是有意的"零限制"设计,不是疏漏。

HTTP 开发

XServer 不包装对象模型:XS_HttpReq 是一组 xrt 指针的视图结构体,你直接使用 xrt 全套 API。

字段说明
tcp / tls传输层裸指针,二选一非空 —— 直接可用 xrt 全套流/发送 API
head解析后的请求头(含 MethodCode);字段视图仅在回调内有效
body分帧状态机:定长 / chunked / 关闭 三种模式,xrtHttp1BodyRead 增量读取
host / server归属信息(vhost 路由结果)

响应构造

响应由应用自行构造 —— xrtHttp1ResponseWrite 生成头、xrtHttp1ChunkWrite 分块、xrtNetStreamSend / xrtTlsStreamSend 发送。下面是 demo 的真实实现(节选):

/* modules/http.h —— 完整响应:Content-Length + Content-Type 统一生成,
 * HEAD 保留 Content-Length 但不发正文 */
static bool ReplyRaw(XS_HttpReq* pReq, uint16 iStatus,
    const char* sContentType, const void* pBody, size_t iBodySize)
{
    char arrHead[1024], arrLength[32];
    xhttpfield arrField[12];
    size_t iFieldCount = 0, iHeadSize = 0;

    snprintf(arrLength, sizeof(arrLength), "%llu",
        (unsigned long long)iBodySize);
    arrField[iFieldCount].Name = XRT_STR_LITERAL("Content-Length");
    arrField[iFieldCount].Value = xrtStrView(arrLength);
    iFieldCount++;

    if ( !xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, iStatus,
        xrtHttpStatusText(iStatus), arrField, iFieldCount,
        arrHead, sizeof(arrHead), &iHeadSize) ) return false;
    if ( !ConnSend(pReq, arrHead, iHeadSize) ) return false;
    return pReq->head->MethodCode == XHTTP_METHOD_HEAD ||
        ConnSend(pReq, pBody, iBodySize);
}

路由(应用层模式)

宿主不内置路由器 —— 每个 host 一个 RequestProc,URL 分发由应用自建。demo-single 验证的模式:静态表(xrtMap)+ 动态 pattern(xrtPatternCompileMany)+ 方法槽:

AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_GET,  Handle_API_List);
AddDynamicRouteHTTP("/api/v1/items/{id}",
    XHTTP_METHOD_PUT | XHTTP_METHOD_PATCH, Handle_API_Item_Update);
/* 注册完成后一次性编译,匹配耗时与路由数无关 */
(void)RouteHTTP_Compile();
  • 方法掩码可自由组合,XHTTP_METHOD_CRUD / XHTTP_METHOD_ANY 为聚合掩码
  • 匹配顺序:静态精确 → 动态 pattern → 方法槽;未命中返回 XS_FALLBACK 落回 wwwroot
  • 万级路由实测:10 条 42.0µs → 10,000 条 59.1µs(基准详情)

vhost 与 TLS

Host 头 / absolute-form / authority-form 三来源路由;HTTP/1.1 强制 Host,未匹配回落默认 host。TLS 按 SNI 选择 host 级证书,服务端 Cookie 把 ClientHello 身份选择与 Accept 绑定到同一代。证书支持热替换(锁内原子换表)。

WebSocket 开发

XServer 完成握手校验(失败 400)并 xrtWsStreamAttach,掩码、分片重组、控制帧、permessage-deflate 全部在 xrt 层处理 —— 你只写业务回调:

static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
    (void)xrtWsStreamText(pWs, xrtStrView("connected"));
}

static void WsText(XS_HostInfo* pHost, xwsstream* pWs, xstrview tText)
{
    (void)xrtWsStreamText(pWs, tText);   /* echo */
}
  • 发送:xrtWsStreamText / xrtWsStreamBinary(另有 Ref / Take 所有权变体)
  • 限制:ws_message_limit 超限以 close 1009 关闭;ws_protocol 可强制子协议
  • 未导出 WsText/WsBinary 的 host 返回 503;vhost 路由失败 404;头超限 431
  • 空闲扫描用跨线程安全的 Abort;累积器超 256KB 在消息结束后释放

TCP 流服务

class = tcp(TLS 由服务级 tls 开关决定 tcp / tcps)。XS_StreamConn 是视图结构体:tcp 与 tls 二选一非空,按非空一侧使用对应发送 API(tcps 下禁止绕过 tls 直写)。

static void EventOpen(XS_HostInfo* pHost, XS_StreamConn* pConn)
{
    /* 连接建立即发欢迎横幅 */
    if ( pConn->tcp != NULL )
        (void)xrtNetStreamSend(pConn->tcp, "hello\n", 6);
}

static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
    /* pBuffer 为零拷贝缓冲链,仅回调内借用;
     * 尺寸查询 xrtNetBufSize(pBuffer),数据经 xrt 缓冲 API 消费 */
}

static void EventClose(XS_HostInfo* pHost, XS_StreamConn* pConn,
    xnetresult iResult, const xerror* pError)
{
    /* 连接生命周期由 xs 持有,应用不负责 Destroy */
}
  • 连接生命周期:Accept 接管 → 注册表 → Close 时析构,全部由 XServer 驱动持有
  • idle_timeout 到点服务端主动关闭;recv_limit 小于默认读块时自动收缩
  • host 证书构建 SNI 表,每连接按 SNI 出证书

UDP 服务

class = udp,最小驱动:bind → EventDgram 透传。每个数据报都经监听槽位取当前代 lease,无连接状态。

static void EventDgram(XS_HostInfo* pHost, xnetudp* pUdp,
    const xnetudpmessage* pMsg)
{
    /* pMsg->Remote 来源地址 · pMsg->Data/pMsg->Size 负载
     * pMsg->Flags 含 XNET_UDP_MESSAGE_TRUNCATED 截断标志 */
    (void)xrtNetUdpSendTo(pUdp, &pMsg->Remote,
        "ack", 3);   /* 回发到来源地址 */
}

recv_limit 上限 65535;发送完全由脚本经 xrtNetUdpSendTo 自理,支持任意目标地址。

热重载

reload 是期望状态协调任务,不是同步命令:Submit 返回稳定 ID,终态可查询。host / server 都以完整 server generation 为发布单元。

触发 API

函数说明
xsReloadHostSubmit(pHost)重载一个 host(以所属 server 为发布单元)
xsReloadServerSubmit(name)按名称重载整个 server
xsReloadAllSubmit()整批事务:全部候选预编译,任何失败逆向回滚
xsReloadQuery(id, &result)查询终态与 Revision 指纹

终态枚举:ACCEPTED → PREPARING → SUCCEEDED / FAILED / SUPERSEDED / CANCELLED。同 server 的新意图覆盖未执行的旧意图(SUPERSEDED),pending 上限 64。

状态交接

/* 旧代:无破坏快照导出(导出后旧代仍可继续服务) */
static bool ServiceSwap(XS_HostInfo* pHost, xvalue** ppShared)
{
    xvalue* pState = xrtValueObject();
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("counter"),
        xrtValueInt64(G_iCounter));
    *ppShared = pState;
    return true;
}

/* 新代:ServiceInit 内取回,无缝续跑 */
static void ServiceInit(XS_HostInfo* pHost)
{
    xvalue* pState = xsSwapTake(pHost);
    if ( pState != NULL ) {
        G_iCounter = xrtValueInt64(xrtValueObjectGet(pState, "counter"));
        xrtValueRelease(pState);
    }
}

语义保证

  • 先编译后换代:整个 server 的候选脚本全部编译成功才进入副作用区 —— 任一失败整体拒绝,旧代原样服务(回滚是语义保证)
  • 双代并行:旧连接继续由旧代码服务完毕;端点不变时监听槽位无闪烁 handoff
  • 引用归零回收:lease 归零后 GC 线程按序析构脚本 / 驱动 / 配置,三循环实测零泄漏零崩溃
  • 定时器随代作废:xsTimerAfter 绑定创建时的旧代,换代自动取消,不会打进新代
  • 不可变项:engine.workers 变更被拒绝;custom 类不支持在线重载(raw 资源无终态 lease)

单文件发布(站点 VFS)

把 xs.json 与整个站点打成一个 LZMA 归档追加到 xs 末尾,得到单文件可部署的 exe。无参启动自动探测应用包,从包内读配置、出文件。

pack 命令
xs.exe pack <站点目录> -o app.exe     # 站点目录须含 xs.json;基底 = 当前 xs
xs.exe pack --list app.exe           # 列出包内条目(路径/方法/大小/CRC)
xs.exe pack --extract app.exe -d out/  # 解包取证
xs.exe pack --strip app.exe -o orig.exe # 还原未打包二进制(字节级一致)

磁盘优先

放同名文件到 exe 旁即覆盖包内版本(热修 CSS / 续签证书免重打包),删除即恢复包内内容。证书路径经统一出口读取——磁盘优先、包内兜底,程序写文件后包内对应文件自然作废。

归档格式

  • 压缩:逐条 LZMA(无收益自动 STORE);demo 站 1.7 MB → 542 KB
  • 探测:读自身末尾 24 字节尾标,零成本;四级 CRC 校验(尾标/归档头/索引/条目)
  • 内存:索引启动装载后不可变(线程安全);内容首次访问惰性解压并常驻缓存
  • --no-vfs 忽略应用包强制目录模式(调试用)

格式逐字段规格见仓库 docs/站点VFS设计.md;端到端测试 python tools/test_site_vfs.py(19 项)。

桌面应用形态(class: "app")

服务节点声明 "class": "app" 即获得桌面窗口形态:本地 HTTP 服务 + WebView2 窗口同进程——启动加载窗、服务就绪自动开界面、关窗优雅停机。http 侧配置(host / static / TLS / 热重载)全部同义,仅多一个 window 描述符(9 字段约束见 字段参考)。

何时选 app 形态

  • 面向最终用户交付工具类产品(管理面板、本地仪表盘、Agent 工作台),用户不该看见"打开浏览器输地址"
  • 同一份产物要兼做服务器部署——Linux / 无 webview 变体自动 headless,配置不用改
  • 需要本地端口隐身:app 类默认 ip 127.0.0.1(不触发防火墙弹窗)+ port: 0 OS 自动分配(实际端口在 XS_ServerInfo.PortBound)

xsw.exe

Windows 发布包附带 xsw.exe——同一构建的 GUI 子系统变体(无控制台黑框),app 形态打包时以它为基底:xs.exe pack 站点/ -o app.exe 产物双击即用;配合 close: "stop" 全程无控制台窗口。

发布 checklist

  • window.devtools 关闭(默认即关)——生产包不开 F12
  • install_fallback 按受众选:ask 面向普通用户(缺 Runtime 弹窗问询)/ silent 无人值守 / headless 明确不要窗口
  • 外部系统需要回调或地址要收藏 → 配固定端口(port:0 粘滞只在进程存活期)
  • close: "hide" 场景确认停机路径(信号;v1 无托盘)
  • 服务器部署同一 xs.json 直接可用:无窗口后端的环境自动 headless,日志打印访问 URL

平台 / 变体降级矩阵与生命周期细节见 webview 参考页;端到端实战见教程 第 50 章。

证书自动续签(acme 节)

需 xacme 扩展。xs.json 根级 acme 节启用内置 ACME 守护——证书生命周期从"手工续期"变为无人值守:每日定时检查,剩余寿命不足 30 天自动 DNS-01 续签,热替换不断连接。全部字段约束见 xs.json 字段参考。

续签规则

项行为
检查时机check_time "03:00" 每日固定时刻(或 check_interval_hours 12 间隔制,两者互斥);启动 5 分钟后首查
触发续签剩余寿命 < 30 天(renew_before_days 可调 1–89),或 store 无证书
短路证书充足时不联系任何 CA——纯本地判断
热替换续签产物原子写盘(acme.sh 同构布局)→ 对引用该证书组的每个 server 调 xsTlsRefresh

CA 回退链

directories[] 有序数组:按序尝试、首个成功者胜出;某 CA 失败(网络/限流/协议)自动落下一个。本地证书充足即短路;到期续签从链首重试——主 CA 恢复后自动回归。多 CA 并存零改造:xacme store 按 CA 分目录存账户,同一把账户私钥可在多个 CA 各自开户。

EAB 与账户导入

  • 需 EAB 的 CA(如 litessl):eab_kid_file / eab_hmac_file 成对引用(凭据不进 xs.json)
  • 已有 acme.sh 账户免 EAB 导入:把账户私钥(SEC1/PKCS8 均可)预填 acme-store/accounts/<sha256(url) 前 16 hex>/account.pem——守护走"store 有账户即复用",EAB 全程不需要

运维速查

  • 强制重签:删 acme-store/certs/<主域名>/ 后重启(判断依据是 store 不是 host tls 目录)
  • 守护线程自建网络引擎(Workers=1)、配置启动时深拷贝——改 acme 节须重启生效
  • 服务化部署参考仓库 tools/xs.service(systemd,崩溃 5 秒自动拉起 + AmbientCapabilities 摆脱 setcap)

部署与运维

交付形态

单文件 xs / xs.exe(约 6.4MB,LZMA 内嵌 TCC + SDK + 系统库)+ 应用源码目录。目标机零安装:无运行时、无 DLL、无头文件依赖。

优雅停机

Ctrl+C / SIGTERM 触发五阶段:停止接入并退役脚本 → 等待网络终态并停引擎 → 退役 custom → 排空 reaper 上的析构 → 释放配置。ServiceUnit 在此路径中保证至多一次执行。

测试基线

  • func_test —— 40 个断言场景:21 个配置 fail-fast 用例、8 种路径逃逸向量、重载风暴
  • reload_matrix —— 四协议 × TLS 共 7 端点同端点 handoff 与代隔离验证
  • drill —— 6 小时长时演练:slowloris / pipeline 炸弹 / 坏脚本重载炸弹 + RSS 采样
  • routebench —— 与 Go / PHP / FastAPI 的同机时延对比(结果)

平台

Windows 与 Linux 双平台(构建系统自动分支);macOS 在 xrt 层有 kqueue 后端支持。中文路径在 Windows 入口处统一转换为 UTF-8。