Chapter Orientation

Volume 12 starts with the toolchain. XRT's build is not a hand-written Makefile but a Python toolchain with config/modules.json as its single source of truth: the manifest declares every module's headers/sources/tests/examples/docs (including the dependency closure), and tools/build.py drives two verification tracks from it — modular compilation (selecting sources per module, caching objects by closure fingerprint) and the single-header regression (tools/amalgamate.py synthesizes single/xrt.h, then recompiles all tests). This chapter makes four things clear: the manifest structure (schema/dependencies/three test-asset classes: tests/single_tests/examples); build.py's CLI face (suite selection/test filtering/parallel jobs/cflag injection/manifest stacking — extension libraries share the same toolchain); why two tracks (modular proves "this trim compiles", single-header proves "the merge conflicts nowhere" — each track is a release gate); fingerprint caching (objects recompile only when the dependency-closure fingerprint changes — the correctness root of incremental builds).

130.1引入

C-library build systems suffer three chronic ills: manifest drift (CMakeLists and the real source files tell different stories — a file added, the list forgotten, or the reverse); fake trimming (claiming configurability but never verifying that trim combinations compile — feature-macro combinations never tested); single-header desynchronization (the single header maintained by hand, a module changed and the merge forgotten). XRT's answer: one JSON manifest as the source of everything — build (what to compile), amalgamate (what to merge), package (what to pack), measure (what to benchmark) all start from the same modules.json. "The manifest drives everything" eliminates drift by construction: a source not in the manifest doesn't compile; a test not in the manifest doesn't enter the regression.

The dual track, expanded: the modular track takes the dependency closure of your chosen suite (say core,queue) and compiles only those sources — verifying "this trim compiles and runs"; the single-header track first merges all selected modules into one single/xrt.h (amalgamate concatenates in topological order, deduplicates includes, inlines local headers), then compiles the same tests as one translation unit with XRT_IMPLEMENTATION — verifying "the merged single header has no symbol clashes or macro leaks". Every PR must pass both tracks before merging — this is the implementation face of the SPEC's "check + embedded examples compiled into CI".

130.2概念

The manifest: each module's declaration card

1modules.json (schema 1)one card per module
2Extension stackingextlibs/*/config/modules.json stacks via the --manifest parameter

The manifest is simultaneously the documentation index (the docs field) and the benchmark registry (the benchmarks field — the queue module's card carries all of dev/bench/queue's scripts and reports) — the full meaning of "source of truth": compile, test, package, measure, document lists — five faces, one card.

build.py's CLI face

ParameterPurpose
--suite core,queuemodule names/comma combinations/all — dependency closure expanded automatically
--test test_queue_oomrun a single modular test
--start-test/--start-single-testresume the regression from a named test (checkpoint restart — fix one, run the rest)
--jobs 8parallel compile jobs for single-header tests (tests still run in manifest order — determinism)
--no-single/--no-examplesskip tracks (modular only / test gate only)
--cflag/--ldflaginject compile/link flags (cross scenarios / sanitizers)
--compiler tcc/--arch x86compiler and architecture choice (TinyCC 32-bit is also a gate combination)
--manifest pathstack an extension library's manifest
--rebuildignore fingerprints and force recompilation

Dual-track builds and fingerprint caching

Modular track: suite→closure→compile objects per module (-DXRT_FEATURE_* generated from the manifest's feature fields)→link→run tests in manifest order; examples compile and run one by one (the tutorial's embedded examples are verified here — the SPEC gate's "embedded examples compile"). Single-header track: amalgamate produces single/xrt.h (plus the declarations-only xrt_decl.h)→single_tests compile with #define XRT_IMPLEMENTATION + include of the single header→run. Fingerprint caching: objects cache by "dependency-closure fingerprint" — any source/header inside the closure changes, the fingerprint changes, only then recompile; --rebuild invalidates explicitly. Architecture matrix: native/x86/x64 × gcc/tcc/cl — the release gates cover 32-bit TinyCC (the stand-in for resource-constrained targets).

Extension libraries share the toolchain

The five extension libraries of Chapters 110–129 each carry a config/modules.json — with --manifest extlibs/xhttp/config/modules.json stacked, the same build.py compiles and tests the extension closure (including "the core trim closure the extension requires"). xruntime's README shows the full gate sequence: the build regression, the amalgamate consistency check (--check), the release-maturity check, package verification — library and extensions, one toolchain, one discipline.

130.3示例

First complete program: a manifest-declared example

The program below is from examples/core/version_limits — a member of the manifest's examples array (both build tracks compile it):

examples/core/version_limits/main.c源码 ↗
















 */

#include <stdio.h>
#include <xrt.h>

int main(void)
{
	xrtresourcelimits Limits;

	printf("version=%s\n", xrtVersion());
	xrtResourceLimitsInit(&Limits);
	printf("limits: depth=%u entries=%llu input=%llu\n",
		(unsigned)Limits.iMaxDepth,
		(unsigned long long)Limits.iMaxEntries,
		(unsigned long long)Limits.iMaxInputBytes);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/core/version_limits/main.c -lws2_32 -liphlpapiversion=2.0.0-devlimits: depth=128 entries=100000 input=268435456

What just happened. (1) This file appears in the core module's examples array — python tools/build.py --suite core compiles and runs it; every tutorial-embedded example since Chapter 3 works the same way (tutorial code is gate code — where the SPEC's red lines are machine-executed). (2) xrtVersion() and xrtResourceLimitsInit are APIs declared in core's public headers — the manifest's public_headers field maps one-to-one onto the docs/api contract cards. (3) The default resource bounds (depth 128 / 100k entries / 256 MiB input) are every parser's shared defense line — Chapter 133's testing system comes back to build DoS tests on these bounds.

Second complete program: the allocator-tour example

The second program is from examples/core/allocator_tour — integration verification for a custom allocator:

examples/core/allocator_tour/main.c源码 ↗



















 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <xrt.h>

/* 计数分配器:仍路由系统堆,便于验证替换生效。 */
typedef struct examplecount {
	size_t iAllocs;
	size_t iFrees;
} examplecount;

static ptr exampleAlloc(ptr pContext, size_t iSize)
{
	++((examplecount*)pContext)->iAllocs;
	return (ptr)malloc(iSize);
}

static ptr exampleRealloc(ptr pContext, ptr pMemory, size_t iSize)
{
	(void)pContext;
	return (ptr)realloc(pMemory, iSize);
}

static void exampleFree(ptr pContext, ptr pMemory)
{
	++((examplecount*)pContext)->iFrees;
	free(pMemory);
}

int main(void)
{
	xallocator Default;
	xallocator Custom;
	examplecount Count;
	uint8* pBlock;
	uint8* pDup;
	int iResult = 1;

	/* ---- 必须在任何 XRT 分配前完成替换。 ---- */
	xrtGetAllocator(&Default);
	memset(&Count, 0, sizeof(Count));
	Custom.Context = (ptr)&Count;
	Custom.Alloc = exampleAlloc;
	Custom.Realloc = exampleRealloc;
	Custom.Free = exampleFree;
	if ( !xrtSetAllocator(&Custom) ) {
		goto Cleanup;
	}

	/* ---- At 族五接口:位置参数不影响堆行为。 ---- */
	pBlock = (uint8*)xrtMallocAt(4u, __FILE__, __LINE__);
	if ( (pBlock == NULL) ||
		((pDup = (uint8*)xrtMemDupAt("hi", 2u, __FILE__,
			__LINE__)) == NULL) ||
		(memcmp(pDup, "hi", 2u) != 0) ||
		((pBlock = (uint8*)xrtReallocAt(pBlock, 16u, __FILE__,
			__LINE__)) == NULL) ||
		(xrtCallocAt(2u, 4u, __FILE__, __LINE__) ==
			NULL) ) {
		goto Cleanup;
	}
	/* Calloc 清零验证单独做——保留指针用于释放。 */
	{
		uint8* pZero = (uint8*)xrtCallocAt(2u, 4u, __FILE__,
			__LINE__);

		if ( (pZero == NULL) || (pZero[0] != 0u) ||
			(pZero[7] != 0u) ) {
			goto Cleanup;
		}
		xrtFreeAt(pZero, __FILE__, __LINE__);
	}
	xrtFreeAt(pDup, __FILE__, __LINE__);
	xrtFreeAt(pBlock, __FILE__, __LINE__);
	printf("allocator: swap -> at-family alloc/calloc/dup/realloc ok\n");

	
	 * 且 GetAllocator 仍报告已安装的计数分配器。 ---- */
	
	 * 分配才经过自定义 Alloc;计数只需 >= 1 即证明替换生效。 */
	if ( xrtSetAllocator(&Default) ||
		(Count.iAllocs < 1u) ) {
		goto Cleanup;
	}
	{
		xallocator Current;

		xrtGetAllocator(&Current);
		if ( (Current.Alloc != Custom.Alloc) ||
			(Current.Free != Custom.Free) ) {
			goto Cleanup;
		}
	}
	printf("allocator: locked after first alloc -> swap rejected ok\n");
	iResult = 0;

Cleanup:
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/core/allocator_tour/main.c -lws2_32 -liphlpapiallocator: swap -> at-family alloc/calloc/dup/realloc okallocator: locked after first alloc -> swap rejected ok

What just happened. (1) After xrtSetAllocator swaps in a custom allocator, the at-family four entrances run (Chapter 5's memory face, regressed at the example level). (2) The locked-after-first-allocation semantics verified: a swapped allocator cannot be swapped again — the stability guarantee for embedding hosts (Chapter 135). (3) The two output lines are themselves products of the dual-track build: manifest-driven, gate-executed — every tutorial example you have seen passed through this chain.

130.4契约

  • Single source of truth: modules.json drives build/amalgamate/package/measure/doc — add the manifest before adding sources; keep both synchronous.
  • Three asset classes: tests (modular) / single_tests (single header) / examples (tutorial + gate) — each with its track.
  • Closure semantics: a suite expands to its dependency closure; compiling/trimming/packing all follow the closure.
  • Dual-track gate: modular proves the trim compiles, single-header proves the merge conflicts nowhere — both green before merge.
  • Deterministic order: tests run in manifest order (jobs parallelize compilation only, never tests).
  • Fingerprint caching: objects cache by closure fingerprint; --rebuild invalidates explicitly.
  • Architecture matrix: native/x86/x64 × gcc/tcc/cl — TinyCC 32-bit is the resource-constrained gate.
  • Checkpoint restart: start-test/start-single-test resume from the breakpoint — fix one, run the rest.
  • Extension stacking: --manifest stacks extension manifests — library and extensions, one toolchain.
  • Examples are gates: examples compile and run in both tracks — tutorial-embedded code with zero extra maintenance.

Common Mistakes

Pitfall 1: adding a source file and forgetting the manifest:Pitfall 1: adding a source file and forgetting the manifest

Symptom: "compiles locally" (IDE full build), but CI's modular track compiles an incomplete closure — link failure, or worse (symbols leaking in from another module).

Cause: the IDE ignores the manifest; build.py compiles only what the manifest lists. The difference between "passes locally" and "passes the gate" is exactly the manifest.

错误写法
/* create src/foo/bar.c and commit directly - the manifest doesn't have it */
/* CI: the modular track's closure is incomplete - link failure */
正确写法
/* add it to the foo module's sources array in modules.json in the same change */
/* the PR diff shows manifest entry and source file together - reviewable consistency */
Pitfall 2: merging after running only the modular track:Pitfall 2: merging after running only the modular track

Symptom: the single-header track explodes — two modules' private symbols/macros clash after the merge; the user's single/xrt.h doesn't compile.

Cause: modular compilation isolates inter-module private-name clashes; only the single-header merge exposes them. Neither track can be skipped.

错误写法
python tools/build.py --suite my_module --no-single && merge
/* skipping the single-header track before merging - clashes blow up in the user's hands */
正确写法
/* before merging a PR: both tracks green - --no-single is only for fast mid-development iteration */
python tools/build.py --suite my_module   /* includes the single-header track */
Pitfall 3: treating --jobs as test parallelism:Pitfall 3: treating --jobs as test parallelism

Symptom: assuming --jobs 8 runs tests in parallel — tests with order dependencies (shared ports/temp files) occasionally interfere.

Cause: --jobs parallelizes compilation only; tests run strictly in manifest order (determinism first — reproducible failures are the gate's value).

错误写法
/* assuming --jobs 8 runs tests in parallel - tests sharing resources occasionally interfere */
/* fact: --jobs parallelizes compilation only; tests strictly follow manifest order */
正确写法
/* want speed: run a subset, not in parallel */
python tools/build.py --suite my_module --test test_queue_oom
/* checkpoint restart: --start-test continues from the failure */

Programming Exercises

Basic: run the core suite on both tracks

python tools/build.py --suite core (no --no-single) — observe both tracks' output. Acceptance criteria: modular and single-header tests all green; examples compile once per track; you can point to fingerprint caching working (a second run skips compiled objects).

Advanced: trim-combination verification

Compile with --suite core,queue,queue_spsc and run all tests; then compare timing with --no-single. Acceptance criteria: the closure contains no unselected module's sources; the trim combination's tests all green.

Challenge: reproducing the extension gate sequence

Run the gate sequence from the xruntime README step by step (build regression / amalgamate --check / package --verify). Acceptance criteria: you understand what each command verifies; you can explain amalgamate --check's "generation consistency" in your own words (regenerating yields zero difference from the committed single header).

Summary

TopicQuick reference
Source of truthconfig/modules.json — compile/test/package/measure/docs, five faces one card
Three asset classestests (modular) / single_tests (single header) / examples (tutorial + gate)
Dual trackmodular proves trimming, single-header proves merging — both green before merge
Fingerprint cacheobjects keyed by closure fingerprint; --rebuild invalidates
Determinismtests in manifest order; jobs parallelizes compilation only
Checkpoint restartstart-test/start-single-test
Architecture matrixnative/x86/x64 × gcc/tcc/cl — TinyCC 32-bit is a quality gate
Extension stacking--manifest stacks — five extension libraries, one toolchain
Examples are gatesexamples compile in both tracks — tutorial code with zero second implementations
cflag/ldflaginject compile/link flags (sanitizers/cross)

📖 API references in this chapter:core (25 entries)error (34 entries)

Click for full signatures, parameter constraints, return values and error notes