Chapter Orientation
From this chapter we enter structured data. xvalue is XRT's dynamic type system: one opaque pointer can hold null/bool/int/float/string/bytes/time/array/object/set/map — everything JSON can express, plus XLON's extended types (Chapter 32) living here directly. Three designs decide the usage experience: exact reads (GetInt accepts only int; a type mismatch fails — the first gate against type confusion), a reference-counted lifecycle (the full flowering of Chapter 3's primitives), and immutable sharing (a value tree can be referenced from many places without copying). It is the parse product of Chapters 31/32/35 and the data source of templates — the foundation of this volume's second half.
31.1引入
The classic puzzle of configuration systems: in one config file, the timeout is a number, the switch a boolean, the tags a list, the nested service an object — C's static types cannot hold data whose "shape is known only at runtime". Hand-written void* plus a type tag is the wheel everyone keeps reinventing, with an extreme pitfall density: type judgments rely on memory, free responsibilities on convention, copy semantics on prayer.
xvalue standardizes that wheel: one type enum (xvaluetype) plus a family of construct/read/test functions, with a reference-counted lifecycle. It is conceptually isomorphic to dynamic languages' value objects (Python's object, JS's value), but puts "type strictness" first — the out-param you get is exactly the type you asked for, with none of "numbers and strings auto-convert" leniency. This strictness is precisely a hard requirement in config parsing: "timeout": "30" (string) and "timeout": 30 (number) are two different error signals; leniency quietly turns the former into the latter, and the bug is buried deep from then on.
31.2概念
The type family and construction
| Type | Construct | Exact read |
|---|---|---|
| null / bool | xrtValueNull / Bool | GetBool |
| int64 / double | xrtValueInt / Float | GetInt / GetFloat |
| string / bytes | xrtValueString / Bytes | GetString (view) / GetBytes |
| time / pointer | xrtValueTime / Pointer | GetTime / GetPointer |
| array / object | ArrayNew / ObjectNew + the Set family | by index / by name |
| set / intmap | SetNew / IntMapNew | sets and integer-keyed maps |
Null is a process-level singleton — the same pointer every time, safe to compare with ==. String reads return a view (borrowing, Chapter 3's convention), not a freshly allocated copy. Numbers get one dedicated lenient channel: xrtValueScalarEqual lets int 2 equal float 2.0 (cross-type numeric semantics), and ValueHash agrees with it (int 2 and float 2.0 hash identically) — a consistency design necessary for "value trees as cache keys".
The philosophy of exact reads
GetInt handed a float value fails rather than silently truncating; if you want numeric leniency, test the type explicitly first, then read. This philosophy shares ancestry with Chapter 26's strict parsing: type confusion surfaces at the read gate, not as a landmine at the consumer. Paired with xrtValueType (query the type) and the Is/IsNumber/IsContainer test family, read code always has the shape "test first, take second; type mismatch goes down the error path".
Lifecycle: reference counting in full
This is Chapter 3's xrtRefRetain/xrtRefRelease primitives applied in full: construction returns count 1; xrtValueRetain shares, xrtValueRelease returns; when a container takes a child value, the container acquires its own reference — you Release your copy right after constructing, and the child's lifecycle is the container's business from then on. Releasing the whole tree returns recursively — the root's Release reaches zero, the entire tree destructs.
Value-tree operations
The two big pieces of the config scenario: xrtValueObjectMerge (object overlay merge — the standard posture of "defaults + user overrides", with REPLACE/keep/report-conflict strategies) and xrtValueSetUnion/Merge (set union — the standard posture of permission merging). xrtValueClone does a shallow clone (shared elements) — the shallow-vs-deep choice gets a controlled experiment in the ownership sample.
31.3示例
Complete program: a full-type tour and exact reads
From the repository sample examples/value/basic/main.c:
#include <stdio.h> #include <xrt.h> */ /* 演示全部基础标量、精确 Getter、类型查询、哈希和数值相等。 */ int main(void) { unsigned char arrBytes[] = { 1, 2, 3 }; int iMarker = 0; xvalue* arrValues[] = { xrtValueInt(2), xrtValueFloat(2.0), xrtValueString(XRT_STR_LITERAL("xrt")), xrtValueBytes((xbytesview){ arrBytes, sizeof(arrBytes) }), xrtValueTime((xtime)1234567), xrtValuePointer(&iMarker) }; xvalue* pTrue = xrtValueBool(true); xstrview Name; xbytesview Data; int64 iVersion; double fVersion; xtime Time; ptr pPointer; bool bTrue; uint64 iIntHash; uint64 iFloatHash; int iResult = 0; for ( size_t i = 0; i < (sizeof(arrValues) / sizeof(arrValues[0])); i++ ) { if ( arrValues[i] == NULL ) { iResult = 1; goto cleanup; } } if ( !xrtValueGetBool(pTrue, &bTrue) || !xrtValueGetInt(arrValues[0], &iVersion) || !xrtValueGetFloat(arrValues[1], &fVersion) || !xrtValueGetString(arrValues[2], &Name) || !xrtValueGetBytes(arrValues[3], &Data) || !xrtValueGetTime(arrValues[4], &Time) || !xrtValueGetPointer(arrValues[5], &pPointer) || !xrtValueHash(arrValues[0], &iIntHash) || !xrtValueHash(arrValues[1], &iFloatHash) || !xrtValueScalarEqual(arrValues[0], arrValues[1]) || !bTrue || (iVersion != 2) || (fVersion != 2.0) || (Data.Size != 3) || (Time != (xtime)1234567) || (pPointer != &iMarker) || (iIntHash != iFloatHash) || (xrtValueType(arrValues[2]) != XVALUE_STRING) || !xrtValueIs(arrValues[2], XVALUE_STRING) || !xrtValueIsNumber(arrValues[0]) || xrtValueIsContainer(arrValues[0]) || !xrtValueTruthy(arrValues[2]) || (xrtValueType(xrtValueNull()) != XVALUE_NULL) ) { iResult = 2; goto cleanup; } printf("%.*s value API v%lld\n", (int)Name.Size, Name.Data, (long long)iVersion); printf("time type: %s\n", xrtValueTypeName(xrtValueType(arrValues[4]))); cleanup: for ( size_t i = 0; i < (sizeof(arrValues) / sizeof(arrValues[0])); i++ ) { xrtValueRelease(arrValues[i]); } xrtValueRelease(pTrue); xrtValueRelease(xrtValueNull()); return iResult; }
What just happened. (1) The construction family builds values one by one: Int, Float, String, Bytes, Time... each constructor returns an owning value with count 1. (2) The exact-read family takes them back one by one: GetInt reads an int successfully and a float with failure — the "you get exactly the type you ask for" gate verified on each type. (3) The test family pairs with ValueTypeName (type enum to lowercase name) — debug output and error messages report stable names like "time". (4) ScalarEqual verifies that 2 equals 2.0 across types, and ValueHash verifies both hash the same — lenient numeric semantics and hash consistency appear as a pair, the precondition for value trees as keys.
Complete program: config merging and set union
From examples/value/collections/main.c:
#include <stdio.h> #include <xrt.h> */ /* 演示对象覆盖合并和保持稳定顺序的 Set 并集。 */ int main(void) { xvalue* pDefaults = xrtValueObject(); xvalue* pOptions = xrtValueObject(); xvalue* pLeft = xrtValueSet(); xvalue* pRight = xrtValueSet(); xvalue* pUnion = NULL; xvalue* pMerged = NULL; int iResult = 1; if ( (pDefaults == NULL) || (pOptions == NULL) || (pLeft == NULL) || (pRight == NULL) || !xrtValueObjectSetNew( pDefaults, XRT_STR_LITERAL("timeout"), xrtValueInt(30) ) || !xrtValueObjectSetNew( pOptions, XRT_STR_LITERAL("timeout"), xrtValueInt(5) ) || !xrtValueObjectMerge( pDefaults, pOptions, XVALUE_MERGE_REPLACE ) || !xrtValueSetAddNew(pLeft, xrtValueString(XRT_STR_LITERAL("read"))) || !xrtValueSetAddNew(pRight, xrtValueString(XRT_STR_LITERAL("write"))) || !xrtValueSetIsDisjoint(pLeft, pRight) ) { goto cleanup; } pUnion = xrtValueSetUnion(pLeft, pRight); pMerged = xrtValueClone(pLeft); if ( (pUnion == NULL) || (pMerged == NULL) || !xrtValueSetMerge(pMerged, pRight) || !xrtValueSetEqual(pMerged, pUnion) ) { goto cleanup; } printf( "options=%zu permissions=%zu\n", xrtValueCount(pDefaults), xrtValueCount(pUnion) ); iResult = 0; cleanup: xrtValueRelease(pMerged); xrtValueRelease(pUnion); xrtValueRelease(pRight); xrtValueRelease(pLeft); xrtValueRelease(pOptions); xrtValueRelease(pDefaults); return iResult; }
What just happened. (1) Object merge Merge(默认, 用户, REPLACE) (defaults, user, REPLACE): the user config's timeout 5 overrides the default 30 — after merging, count is still 1; overriding adds no key. (2) Set union takes two roads: Union produces a new set and Merge merges in place, the products tested equal with SetEqual — different roads, same destination. (3) IsDisjoint decides two sets are disjoint — permission checking's "any intersection?" answered in one step. (4) These two operations together are the configuration-layer skeleton of "default config + user override + permission merge"; Chapter 38's templates and the value trees read out by Chapter 31's JSON use them directly.
31.4契约
- Exact reads: type mismatch fails, no implicit conversion; numeric leniency goes through explicit
ScalarEqual. - Reference counting: construct = 1,
Refshares,Releasereturns, zero destructs the whole tree; a container attaching a value acquires its own reference. - Null singleton: one pointer process-wide, comparable with
==. - String borrowing:
GetStringreturns a view whose validity follows the value tree. - Hash consistency:
ValueHashpairs withScalarEqual— value trees can be cache keys. - Merge strategies: REPLACE overwrite / keep existing / report conflict — chosen explicitly.
From examples to engineering: three hosts of value trees
Three typical hosts of value trees in engineering decide how the reference counting balances. Config trees: parsed at startup (Chapter 31's JSON), constructed once, read-only throughout, root-Released once at exit — the simplest balancing (construct and root-release only, no Ref/Release in between). Message passing: a value tree travels as a message across threads/modules — the sender Retains then hands over, both sides Release; or use SetTake to transfer (the standard posture in Chapter 18's queue scenario — one transfer, zero surplus references). Cache sharing: one value tree referenced by many consumers (default config shared with all requests) — the constructor Retains, puts it into a global, consumers Retain/Release individually; COW (copy-on-write) semantics make a shallow-shared Clone truly copy only on first edit. Beyond the three hosts, one master discipline: write the balancing into the design document, don't pray at runtime — Chapter 6's statistics are the final judge of balance correctness.
The division of labor with containers
What is the relation between Chapter 30's value containers (array/object/set/intmap) and Volume 3's same-named containers (Chapters 14/18/19)? Two parallel implementations serving two worlds. Volume 3's containers are the statically typed world: element types fixed at compile time, no type tags, maximum performance — hot-path data structures use them. Value containers are the dynamically typed world: every value carries a type tag, heterogeneous mixing works, serialization is supported — config and protocol data use them. Selection mnemonic: data shape known at compile time → Volume 3 containers; known only at runtime (from JSON/network/scripts) → value trees. The two can bridge (a value tree's intmap converts with xintmap), but don't mix semantics — using a value tree as a high-performance container, or conversely adding type tags to static containers, both re-invent the other.
Common Mistakes
Symptom: memory leak — after the value tree is released, statistics still show live values; the leak volume is proportional to "the number of child values attached into containers".
Cause: attaching into a container increments the reference (the container's copy); the reference you got at construction is still in your hand — un-Released, it stays one too many forever.
xvalue* pTimeout = xrtValueInt(30); xrtValueObjectSet(pConfig, XRT_STR_LITERAL("timeout"), pTimeout); /* Release missing: pTimeout's reference count sits at 2; after the container frees, 1 remains - a leak */
xvalue* pTimeout = xrtValueInt(30); xrtValueObjectSet(pConfig, XRT_STR_LITERAL("timeout"), pTimeout); xrtValueRelease(pTimeout); /* the container already holds a reference - return your own copy */
Symptom: a number written as a string in the config ("30") — the program "runs" but its behavior drifts with the upstream format; the landmine detonates during refactoring.
Cause: the discipline of testing with IsNumber first was replaced by the "just try GetInt" mindset — leniency masks an upstream data-shape error.
int64 iTimeout; if ( xrtValueGetInt(pValue, &iTimeout) ) { /* "30" (string) fails here - but worse, someone will add a fallback that retries as a string */ }
if ( xrtValueType(pValue) == XVALUE_INT ) { int64 iTimeout; xrtValueGetInt(pValue, &iTimeout); } else { ReportTypeMismatch("timeout", "int"); /* the shape error is reported at the read gate */ }
Programming Exercises
Construct one of every scalar type, print each's ValueTypeName and ValueType; call GetFloat on an int value and verify the failure — the feel of strictness.
Build two object trees, "default config" and "user config" (3~4 keys each, nested objects included), merge with REPLACE and print all key-values; then switch to the report-conflict strategy and feed a key-colliding input to watch it fail. Hint: think through the nested-object merge semantics too (replace whole or merge recursively — decide by your business and write it in a comment).
Store "config name → value tree" in Chapter 18's map, fingerprint the tree with ValueHash and test equivalence with ScalarEqual; the same config parsed twice must share a fingerprint; changing one field must change it. Acceptance criteria: the fingerprint is stable enough for test assertions; cached-hit and cached-miss trees compare equal via ScalarEqual; after releasing everything, Chapter 6's statistics verify zero leaks.
Summary
| Topic | Quick reference |
|---|---|
| Type family | scalars + string/bytes/time/pointer + array/object/set/intmap |
| Exact reads | type mismatch fails; numeric leniency via ScalarEqual; type testing via ValueType |
| Lifecycle | construct = 1 / Ref shares / Release returns; a container attaching a value holds its own reference |
| Null singleton | the same pointer compares with ==; to distinguish "absent" from "value is null", test the type with XVALUE_NULL |
| Hash | ValueHash pairs with ScalarEqual — value trees can be cache keys |
| Config duo | ObjectMerge (REPLACE/keep/conflict) + SetUnion/Merge |
| Strings | GetString returns a borrowed view; Dup it yourself if you need a copy |
📖 API references in this chapter:value (159 entries)
Click for full signatures, parameter constraints, return values and error notes