Chapter Orientation
xtemplate standardizes "data + template → text": compile/render separation — xrtTemplateCompile parses the template into an immutable node tree (reference counted, the same lifecycle as Chapter 30's regex), and xrtTemplateRender renders owning text from a value tree (Chapter 31's xvalue). Three interpolation prefixes cover three common needs: {$path} direct output, {%path:fmt} number formatting (the format string is Chapter 26's syntax), and {&path:fmt} time formatting; control structures (loops/conditionals) and function calls unfold in the tour sample. The standard usage for config templates, report headers, and notification messages: compile at startup, render at high frequency — the parsing cost is paid once. This chapter shares its two-layer lifecycle mental model with Chapter 30's regex; learn one and the other comes at zero cost.
35.1引入
A notification system's need (nearly every online system has this module): "Dear {username}, your order {order-id} shipped at {time}." — three fields, three types (string, number, time), plus channel variants (a short SMS version, a long mail version). Hand-assembling with xrtFormat means fetching each field, formatting each, concatenating each — noodles as soon as fields multiply; Chapter 25's builder is better, but the "template" still lives scattered in code — changing one wording means changing code, recompiling, redeploying.
The template engine pulls the "shape" out of the code: the template is data (a file, a string in config) — change the wording, change the template: no release, no downtime; rendering is a pure function — same template plus same data always yields the same output, testable and cacheable. Compile/render separation adds the performance link (its essential difference from "string replacement"): template parsing (building the node tree, validating paths) happens once at compile time, and rendering does only "fetch + format + write" — the cost of the hot path (rendering a notification per request) is essentially independent of template complexity.
35.2概念
Compile/render separation (memorize the model before the functions)
The compiled object is immutable and shareable — multiple threads rendering the same template need only their own value trees (rendering only reads the template). This is fully isomorphic with Chapter 30's regex two-layer model: the heavy operation (parsing) once, the light operation (rendering) at high frequency.
Three interpolation prefixes (a one-character semantic division each)
| Prefix | Syntax | Semantics |
|---|---|---|
| Direct output | {$user.name} | the string as-is; the path navigates the value tree by dots |
| Number formatting | {%count:,d} | after the colon, Chapter 26's format string (thousands, base, percentage) |
| Time formatting | {&updated:%F} | time values rendered by format string (ISO date, etc.) |
The path syntax a.b.c navigates the value tree (dot access into nested objects, consistent with most template languages) — at render time the value is fetched by path; a miss (path absent or type mismatch) is handled per the render configuration (error or empty string, chosen explicitly). Typed formatting is the watershed between templates and "string replacement" (data keeps its type, display keeps its flexibility — the same number field takes the compact format in the SMS template and thousands in the report template; the template decides): number and time formatting are declared in the template, not pre-formatted into strings in code before being stuffed in — data keeps its type, display keeps its flexibility.
Control structures and functions (letting shape adapt to data)
Templates are more than interpolation: loops (walking a value-tree array, one loop variable per item) and conditionals (branching on a value's truth) make "data shape decides output structure" possible — reports loop by row, notifications branch on attachment presence. Function calls (the tour sample's call form) register reusable formatting logic as template-invocable units — display logic like case conversion and plural forms stays on the template side. The boundary discipline for control and functions: templates make display decisions, not business computations — the moment a complex expression appears in a template, move it back to the code layer's data preparation.
Error location (the template's compile-time defense line)
Compile errors carry line numbers (the tour sample verifies error-location line=1) — a syntax error inside the template surfaces at compile time, not as mangled rendered text. Render errors (path misses) are likewise reported structurally — Chapter 4's error system applies as usual.
35.3示例
Complete program: compile, value tree, and the three interpolations
From the repository sample examples/template/core/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xvalue* pData = xrtValueObject(); /* 渲染数据:一个对象 */ xtemplate* pTemplate; str sOutput; xtime Time; */ if ( (pData == NULL) || !xrtDateTime( 2026, 4, 2, 12, 30, 0, 0, &Time ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("name"), xrtValueString(XRT_STR_LITERAL("xrt")) ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("requests"), xrtValueInt(42) ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("created"), xrtValueTime(Time) ) ) { xrtValueRelease(pData); return 1; } /* 编译:一次解析成不可变对象;模板语法错在此步报错。 */ pTemplate = xrtTemplateCompile( XRT_STR_LITERAL( "{$name} handled {%requests:,d} requests on {&created:%F}" ) ); if ( pTemplate == NULL ) { xrtValueRelease(pData); return 2; } /* 渲染:NULL 配置 = 默认预算(防失控输出的上限仍生效)。 */ sOutput = xrtTemplateRender(pTemplate, pData, NULL); if ( sOutput == NULL ) { xrtTemplateRelease(pTemplate); xrtValueRelease(pData); return 3; } puts(sOutput); /* 三个拥有式资源逆序释放。 */ xrtFree(sOutput); xrtTemplateRelease(pTemplate); xrtValueRelease(pData); return 0; }
What just happened. (1) The data side is built with Chapter 31's value tree: ObjectNew + SetNew (String/Int/Time fields of each type) — the template eats value trees, so the products of JSON/XLON parsing (Chapters 32/33) plug in directly, zero conversion. (2) {$name} outputs the string directly; {%count:,d} runs the number through Chapter 26's thousands formatting (42 without separators is still legal); {&day:%F} renders the time as an ISO date — the three prefixes each take one turn. (3) Render returns owning text, freed with xrtFree; the compiled object is returned with Release. Swap in another value tree and the same code produces another notification — the separation of template and data is at its most vivid here.
Complete program: control structures and loops
From examples/template/control/main.c, list loops and conditional branches:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xvalue* pRoot = xrtValueObject(); xvalue* pUsers = xrtValueArray(); xtemplate* pTemplate; str sOutput; */ if ( (pRoot == NULL) || (pUsers == NULL) || !xrtValueArrayAppendNew( pUsers, xrtValueString(XRT_STR_LITERAL("Alice")) ) || !xrtValueArrayAppendNew( pUsers, xrtValueString(XRT_STR_LITERAL("Bob")) ) || !xrtValueObjectSetTake( pRoot, XRT_STR_LITERAL("users"), &pUsers ) ) { xrtValueRelease(pUsers); xrtValueRelease(pRoot); return 1; } /* 一条模板串演示全部控制语句(见文件头语法速查)。 */ pTemplate = xrtTemplateCompile(XRT_STR_LITERAL( "{#if:users}Users: {#foreach:users}" "{?loop.first::, }{$loop.value}{#end}{#else}No users{#end}; " "range: {#for:1:5}" "{#if:loop.value = 2}{#continue}{#end}" "{#if:loop.value = 4}{#break}{#end}" "{%loop.value}{#end}" )); if ( pTemplate == NULL ) { xrtValueRelease(pRoot); return 2; } sOutput = xrtTemplateRender(pTemplate, pRoot, NULL); if ( sOutput == NULL ) { xrtTemplateRelease(pTemplate); xrtValueRelease(pRoot); return 3; } puts(sOutput); xrtFree(sOutput); xrtTemplateRelease(pTemplate); xrtValueRelease(pRoot); return 0; }
What just happened. (1) The loop structure walks the value tree's user array — each element enters the loop variable and the template segment renders repeatedly; the comma-separated output shape is decided by the template fragment inside the loop body. (2) Conditionals and ranges: range: 13 shows the output of conditional/aggregate structures — control structures let the template adapt to the data's shape, not the data bend to a fixed template. (3) The tour sample is left as further reading: it covers loading templates from files, streaming render output, function calls and registries — the four capability groups are the template's full form in real projects, with the four ok lines corresponding one-to-one.
35.4契约
- Two-layer model: the compiled object is immutable and shared by reference counting; rendering only reads the template and runs at high frequency concurrently (each thread with its own value tree).
- Three prefixes:
{$}direct output /{%:fmt}Chapter 26 number format string /{&:fmt}time format string. - Data is a value tree: render input is xvalue — JSON/XLON products plug in with zero conversion.
- Error location: compile errors carry line numbers; render errors (path/type) are reported structurally.
- Boundary discipline: templates make display decisions, not business computations — complex expressions move back to the data-preparation layer.
- Owning products: render output is
str, freed withxrtFree; streaming rendering can wire straight to a sink.
From examples to engineering: three hosts of templates
Notification/message generation (most common): compile global templates at startup, build a value tree when events arrive, render and send — Pitfall 1's good form is exactly its skeleton; multilingual scenarios compile one per language and select by the user's language. Reports and page skeletons: data rows render in loops, headers branch on conditions — the main battleground of control structures; large reports use streaming renders wired to sinks (the tour sample's streamed form), a million rows never assembling a whole page in memory. Config-driven UI copy: copy templates stored as config (change copy without a release), fields fetched from the config value tree — both template and data become hot-updatable assets. The shared evolution path of the three hosts: template from code constant → config file → hot-updatable asset, each step leaning on the two-layer design of "compiled object replaceable, rendering stateless".
The division-of-labor map with JSON/XLON/regex
The second half of Volume 4 has four modules (plus the value tree) frequently sharing the stage; one map prevents confusion. JSON/XLON (Chapters 32/33): structured data in and out — parse into value trees; serialization of value trees back out. Value tree (Chapter 31): the intermediate representation — the common currency of all modules. Regex (Chapter 30): extracts structure from unstructured text (log fields, config lines) → produces value trees. Templates (this chapter): generates text from value trees (notifications, reports) → produces strings. Extraction and generation run in opposite directions, mirrors of each other — Chapter 36's combination chapter chains "extract → value tree → template-generate" into a complete pipeline. On the map, every module does exactly one thing; composition goes through the value tree, the intermediate currency.
A design view: templates are interfaces too
Between template and rendering code sits an implicit interface: the set of paths and types the template references. When the interface changes (a field renamed, a type moving from int to string) and the template doesn't follow, rendering errors at runtime. Three habits make the implicit explicit: a rendering smoke test (minimal value tree + all-prefix render with asserted output) enters CI; template files live in the same repository and review as code; the field-change checklist gains a column, "affected templates". Chapter 36's combination chapter will extend this "interface" idea into a data contract for the complete pipeline.
Common Mistakes
Symptom: abnormal CPU on the high-frequency notification/report path; the hotspot sits in xrtTemplateCompile — fully isomorphic with Chapter 30's regex Pitfall 1.
Cause: compiling (parsing into a node tree) is the heavy operation; rendering is the light one. The heavy operation got into the loop.
str render(const xvalue* pData, xstrview Tpl) { xtemplate* T = xrtTemplateCompile(Tpl); /* compiled on every render */ str s = xrtTemplateRender(T, pData, NULL, NULL); xrtTemplateRelease(T); return s; }
/* compile once at startup; template changes go through "recompile + atomic swap" */ static xtemplate* gTpl; str render(const xvalue* pData) { return xrtTemplateRender(gTpl, pData, NULL, NULL); /* only the light path */ }
Symptom: templates grow longer and increasingly resemble another programming language; changing business rules means changing templates; template tests can't cover the logic branches.
Cause: the template engine offers expressiveness and the team moved data preparation into it — the boundary between "display decisions" and "business computation" fell.
<!-- 模板里做业务计算:满减规则、运费、税费全在模板表达式里 -->
Total: {%cart.subtotal - discount(cart.promo) + shipping(cart.weight) + tax(...):,.2f}<!-- 模板只展示;计算在代码层完成、结果进值树 -->
Total: {%order.total:,.2f}
(order.total 由代码层按业务规则算好再构造值树)Programming Exercises
Build a value tree with a string, a number, and a time field; write a template rendering each with its prefix; swap in another data set and re-render to verify template reuse.
Load a user array into the value tree (name + each one's order count); loop-render the summary line "everyone {name}({count})"; output "no orders" for an empty array — conditionals and loops combined.
A JSON config (Chapter 32) defines report metadata (title, columns, data paths); the template dynamically renders the header and data rows per the config — column count and names all come from config, not hard-coding. Acceptance criteria: the same template plus two different configs renders two different tables; a missing column reports an error with the path; compilation happens exactly once (verified across multiple renders).
Summary
| Topic | Quick reference |
|---|---|
| Two-layer model | compile immutable and shareable (Retain/Release) / render read-only, high-frequency concurrent |
| Three prefixes | {$path} direct output / {%path:fmt} number format string / {&path:fmt} time format string |
| Data | value trees plug in — JSON/XLON products zero-conversion; the value tree is every module's common currency |
| Control | loops walk arrays / conditionals branch / function calls (display logic to the template, business computation to code) |
| Errors | compile errors carry line numbers; render errors structured (path/type) |
| Performance discipline | compile once, share globally; hot paths only render; the same two-layer model as regex |
| Boundary | display decisions to the template, business computation to the code layer |
| Three hosts | notification generation / report pages (streaming renders) / config-driven copy — every evolution leans on the two-layer design |
| Division map | JSON/XLON move structure in and out, the value tree is currency, regex extracts, templates generate |
| Interface view | the paths and types a template references are an implicit interface — render smoke tests in CI prevent drift |
📖 API references in this chapter:template (51 entries)value (159 entries)
Click for full signatures, parameter constraints, return values and error notes