Typed bindings
Validation answers whether a document is valid. Typed bindings also convert between a valid document and generated C structures, so applications can read values and produce documents without handling XML text or parser events.
Bindings are optional. zxsdc --bindings writes two more files beside the
validator, and the reader and writer live in src/zxsd_bind.c. They are
interpreted from constant descriptor tables, like the validator, and never use
the heap.
Generated bindings check runtime, descriptor, and binding ABI epochs at compile
and link time. See API, ABI, and migration before upgrading or linking a separately
built runtime; use zxsd_bind_check_runtime() at that boundary. Regenerate
all binding headers and sources together when the XSD or options change.
Generating bindings
$ python3 zxsdc device.xsd -o generated --bindings
This produces device.zxsd.[ch] (the validator, byte-identical to a build
without --bindings) and device.bind.[ch]. A structure is generated for
every complex type reachable from a bound root, and four functions are
generated for each global element with a complex type:
zxsd_result_t device_device_read(const char *xml, device_Device_t *object,
zxsd_bind_arena_t *arena);
zxsd_result_t device_device_write(const device_Device_t *object,
zxsd_bind_sink_fn sink, void *context,
zxsd_stream_t *validator);
zxsd_result_t device_device_write_buffer(const device_Device_t *object,
char *buffer, size_t capacity,
size_t *length);
const zxsd_bind_root_t *device_device_binding(void);
--bind-root NAME restricts generation to selected roots, by local name or
as {namespace}name. It can be repeated. Use it when another root of the same
schema uses a construct that bindings cannot represent.
Generation is all or nothing: if any reachable construct cannot be represented
faithfully, zxsdc fails with a diagnostic starting with bindings: and
writes no files.
C representation
XSD type |
C type |
|---|---|
|
|
|
|
|
|
string-derived restriction with enumerations |
generated C |
every other simple type, including |
|
float, double and decimal stay textual on purpose: the conversion is
then exact and independent of the C library’s locale and floating-point
formatting support. Restrictions of a native type keep the native type; their
facets are enforced by validation.
Occurrence |
Member |
|---|---|
required value |
the value; a required child structure is embedded by value |
optional scalar or enumeration |
|
optional string |
|
optional child structure |
|
|
|
recursive child structure |
|
Attributes come first, then child elements in content-model order, then the
value member of a simpleContent type. Derived types are flattened: an
extension contains the inherited attributes and elements. Members keep the XML
local name. Names that are C or C++ keywords get a trailing underscore, and an
attribute and an element with the same name are distinguished with _attr or
_elem. Elements of a choice are optional members; exactly one must be
set when writing.
Reading
static unsigned char storage[2048];
zxsd_bind_arena_t arena;
device_Device_t device;
zxsd_bind_arena_init(&arena, storage, sizeof(storage));
zxsd_result_t r = device_device_read(xml, &device, &arena);
if (r.code == ZXSD_OK) {
printf("%s has %zu samples\n", device.name, device.sample_count);
}
The document is validated first, with the same rules as zxsd_validate().
Strings, arrays and optional structures are then allocated in the arena, so
the structure stays valid as long as the arena storage does. Text is
entity-decoded, CDATA is included, and the type’s whiteSpace facet is
applied. Schema defaults are applied: an absent attribute with a default or
fixed value is reported as present, and an element present with empty content
takes its default.
On failure the structure is zeroed and the arena is restored, so no pointer
refers to released storage. ZXSD_ERR_RESOURCE_LIMIT means that the arena
is too small. Measure the largest document the device accepts and keep a
margin: the peak includes the decoded text of numeric values, which is
released once they are converted.
Reading runs after the validation pass has returned and does not add to its
stack peak: it keeps one parser state of about 7 KiB with the default limits
and recurses once per nesting level with a small frame, bounded by
ZXSD_MAX_DEPTH. Validation therefore sets the peak. Its per-level text
buffers are sized by ZXSD_MAX_TEXT_LENGTH: on qemu_x86, the Zephyr
sample peaks at about 26 KiB with the default of 1024 bytes and 15.7 KiB with
128 bytes.
A valid document can still be impossible to represent. xsi:type naming a
derived type, or xsi:nil="true", makes the reader return
ZXSD_ERR_UNSUPPORTED instead of silently dropping information.
Reading fragments with typed callbacks
Select a root or a nested element at generation time:
$ python3 zxsdc met_weather.xsd -o generated \
--stream-bind weatherdata/product/time
This emits the ordinary .zxsd.[ch] validator, plus .bind.[ch] containing
the selected type and a typed reader/callback interface. Full-document object
bindings are not enabled unless --bindings is also passed. Unselected
ancestors and siblings need not be representable as C structures: they still
receive complete schema validation, including facets and identity constraints.
The selected subtree itself follows the ordinary binding support contract.
static met_weather_weatherdata_product_time_reader_t reader;
static met_weather_timeType_t forecast;
static unsigned char storage[4096];
static zxsd_bind_arena_t arena;
static zxsd_bind_action_t on_forecast(void *context,
const met_weather_timeType_t *value)
{
/* Copy needed values into application-owned storage here. */
return ZXSD_BIND_CONTINUE;
}
zxsd_bind_arena_init(&arena, storage, sizeof(storage));
zxsd_result_t r = met_weather_weatherdata_product_time_reader_init(
&reader, &forecast, &arena, on_forecast, application_context, NULL);
/* For every received HTTP body fragment, while r.code == ZXSD_OK: */
r = met_weather_weatherdata_product_time_reader_feed(&reader, bytes, count);
/* Once the complete HTTP body has arrived: */
r = met_weather_weatherdata_product_time_reader_finish(&reader);
met_weather_weatherdata_product_time_reader_dispose(&reader);
In CMake use zxsd_generate(TARGET app SCHEMA met_weather.xsd STREAM_BIND
weatherdata/product/time). STREAM_BIND accepts a list of paths. On Zephyr,
enable CONFIG_ZXSD_BINDINGS. The generic functions are
zxsd_bind_stream_init/feed/finish/dispose(); generated wrappers adapt their
callbacks without nonportable function-pointer conversions.
Path rules
Paths are finite absolute element paths, optionally starting with /.
Local names work when unambiguous; use {namespace-uri}name for an explicit
expanded name. URI slashes inside braces are preserved. There are no prefixes,
wildcards, predicates, descendant axes, attributes, or runtime XPath engine.
Unknown or ambiguous steps, duplicate selections, and selected constructs
outside the binding subset fail generation explicitly. An element with a
simple type is delivered as a generated structure with a value member.
Finite paths through recursive schema types are supported.
Each repeated --stream-bind option generates an independent reader for
that selection, not simultaneous subscriptions on one reader. Reader symbols
join local path names with underscores; deterministic suffixes resolve naming
collisions. The ordinary binding output stays unchanged without the option.
Lifetime and validation
The raw XML is parsed and validated once. A callback runs after the selected element closes successfully. Values split across fragments are reassembled by the validator’s existing bounded text storage, then converted with the same normalization, native integer/boolean/enum, and default rules as the full-document reader. No XML substring is retained or reparsed for binding.
The caller provides a reusable output structure and an arena. Their contents and pointers are valid only during the callback. The output is cleared and the arena restored to its initial usage after each delivery, on error, and on dispose. Do not feed, finish, dispose, or reset the arena from inside the callback. Copy retained values into independent application storage.
Return ZXSD_BIND_CONTINUE to receive later occurrences,
ZXSD_BIND_VALIDATE_ONLY to stop materializing but keep validating all
remaining input, or ZXSD_BIND_CALLBACK_ERROR to stop with
ZXSD_ERR_OUTPUT. Later invalid XML or a failed key/keyref can invalidate
the document after a callback already ran. Treat deliveries as provisional
and only publish application results after a successful *_reader_finish().
The weather sample follows this rule even after retaining its first summary.
Memory and failure rules
There is no implicit heap allocation. Streaming binding adds a fixed per-depth
binding stack to zxsd_xml_reader_t. Only the currently selected subtree is
materialized. Its arrays grow geometrically in the explicit arena, without
input-based preallocation from maxOccurs. Old array blocks remain in the
bump arena until delivery, so reserve growth/alignment overhead as well as
strings and nested structures. No repeated value is silently truncated.
The arena must fit the largest selected occurrence, not the whole response.
Choose a finer selection when one subtree is too large.
ZXSD_ERR_RESOURCE_LIMIT reports arena exhaustion; parser/validator capacities
keep their ordinary errors. The first failure is sticky, and no later callback
runs after it. A derived xsi:type or unsupported nil inside a materialized
subtree is rejected, while unselected dynamic content is still validated.
For document-wide identity constraints, pass an explicit scratch allocator to
the generated initializer; the same bounded canonical-XML scratch requirement
as the event validator applies. Dispose releases that scratch even on error.
Writing
static const int32_t samples[] = {7, 9};
device_Device_t device = {0};
char xml[256];
size_t length;
device.id = 42u;
device.name = "sensor";
device.sample = samples;
device.sample_count = 2u;
zxsd_result_t r = device_device_write_buffer(&device, xml, sizeof(xml), &length);
*_write_buffer() writes a NUL-terminated document and validates it with
zxsd_validate(), so facets, choices and identity constraints are checked
before the result is trusted. *_write() streams to a callback instead. Pass
a streaming validator initialized for the same root to validate while writing
without keeping the document in memory.
The writer itself rejects what the C representation can express incorrectly:
a required NULL string or structure, an array count outside its occurrence
bounds, an out-of-range enumeration, text that is not valid UTF-8 or contains
characters outside XML 1.0, and object graphs nested deeper than
ZXSD_MAX_DEPTH, which includes accidental cycles. Output is deterministic:
namespaces are declared once on the root as ns1, ns2 and so on, and
characters that XML would otherwise normalize, such as tab and line feed in
attributes or carriage return in text, are written as character references.
The Zephyr application in samples/zephyr/bindings reads, updates and writes
a settings document with these functions on qemu_x86.
samples/zephyr/bindings-stream handles the same document with the
fragment-based APIs only: a --stream-bind reader fed in fragments, a
writer sink with a streaming validator, and a raw zxsd_xml_reader_t on the
receiving side.
Zero-heap builds
Reading and writing never allocate. Validation of identity constraints needs
scratch memory exactly as for zxsd_validate(). With ZXSD_NO_HEAP, use
zxsd_bind_read_with_allocator() and
zxsd_bind_write_buffer_with_allocator() with the root returned by
<schema>_<root>_binding(), or initialize the streaming validator with
zxsd_stream_init_with_allocator(). On Zephyr, enable
CONFIG_ZXSD_BINDINGS.
Constructs that cannot be bound
These constructs are fully supported by the validator. Bindings reject them because a fixed C structure cannot represent them without losing information:
element wildcards (
xs:any) and attribute wildcards (xs:anyAttribute);mixed content and
xs:anyTypeelements;repeating
choiceandallgroups, finite repeatingsequencegroups, and unboundedsequencegroups with required members, since their occurrence correlation would be lost;the same element name at several places of one content model;
nillable elements, abstract elements, and heads of substitution groups;
abstract complex types, which are only usable through
xsi:type;QNameandNOTATIONvalues, whose meaning depends on namespace declarations.
An unbounded sequence whose members are all optional and uniquely named is
representable: each member is generated as its own typed array. This supports
schemas such as MET Norway’s weather format without discarding XML content.