XML Module
GX includes an XML module powered by ezxml, a lightweight tree-based XML parser in C (~1000 lines, MIT license). It supports parsing XML from strings or files, navigating the tree, extracting data, building new XML documents, and serializing back to strings.
Prerequisites
The module ships with ezxml bundled — no external dependencies needed.
The C source (ezxml.c) is compiled automatically via @cfile.
Usage
import xml
Build with -I modules:
gx myapp.gx -I modules -o myapp.exe
Quick Example — Parsing a Raylib Tilemap
import xml
fn main() {
// Parse a Tiled-style tilemap XML
var data = "<map width='10' height='8' tilewidth='16' tileheight='16'><tileset firstgid='1' source='terrain.tsx'/><layer name='Ground' width='10' height='8'><data>1,1,2,2,1,1,2,2,1,1</data></layer><objectgroup name='Entities'><object id='1' name='player' x='32' y='48' width='16' height='16'/><object id='2' name='enemy' x='128' y='96' width='16' height='16'/></objectgroup></map>"
var doc = xml.gx_xml_parse(data.cstr, data.len)
if (xml.gx_xml_has_error(doc)) {
print("XML error: {xml.gx_xml_error(doc)}\n")
xml.gx_xml_free(doc)
return
}
defer xml.gx_xml_free(doc)
// Read map attributes
var map_w = xml.gx_xml_attr(doc, "width")
var map_h = xml.gx_xml_attr(doc, "height")
var tw = xml.gx_xml_attr(doc, "tilewidth")
print("Map: {map_w}x{map_h} tiles, {tw}px each\n")
// Read tile layer data
var layer = xml.gx_xml_child(doc, "layer")
var layer_name = xml.gx_xml_attr(layer, "name")
var tile_data = xml.gx_xml_txt(xml.gx_xml_child(layer, "data"))
print("Layer '{layer_name}': {tile_data}\n")
// Iterate objects (e.g. game entities to spawn)
var group = xml.gx_xml_child(doc, "objectgroup")
var obj = xml.gx_xml_child(group, "object")
while (obj != 0) {
var name = xml.gx_xml_attr(obj, "name")
var x = xml.gx_xml_attr(obj, "x")
var y = xml.gx_xml_attr(obj, "y")
print("Entity '{name}' at ({x}, {y})\n")
obj = xml.gx_xml_next(obj)
}
}
Output:
Map: 10x8 tiles, 16px each
Layer 'Ground': 1,1,2,2,1,1,2,2,1,1
Entity 'player' at (32, 48)
Entity 'enemy' at (128, 96)
Parsing
| Function | Signature | Description |
|---|---|---|
gx_xml_parse | (cstr, i64) → *void | Parse XML string with length |
gx_xml_parse_file | (cstr) → *void | Parse XML from file path |
gx_xml_free | (*void) | Free parsed document (root only) |
// From string (e.g. embedded shader metadata)
var doc = xml.gx_xml_parse(text.cstr, text.len)
defer xml.gx_xml_free(doc)
// From file (e.g. Tiled .tmx map)
var map = xml.gx_xml_parse_file("assets/level1.tmx")
defer xml.gx_xml_free(map)
Important: gx_xml_parse copies the input string internally, so the
original is safe. Only call gx_xml_free on the root node returned by
gx_xml_parse or gx_xml_parse_file — never on child nodes.
Error Checking
| Function | Signature | Description |
|---|---|---|
gx_xml_error | (*void) → cstr | Error message (empty string if none) |
gx_xml_has_error | (*void) → bool | True if parsing failed |
var doc = xml.gx_xml_parse(data.cstr, data.len)
if (xml.gx_xml_has_error(doc)) {
print("Parse error: {xml.gx_xml_error(doc)}\n")
xml.gx_xml_free(doc)
return
}
Errors include line numbers, e.g. [error near line 5]: unexpected closing tag </foo>.
Navigation
All navigation functions return node handles (*void). A return value of 0
means “not found.” Never free child nodes — only free the root.
By Tag Name
| Function | Signature | Description |
|---|---|---|
gx_xml_child | (*void, cstr) → *void | First child with given tag name |
gx_xml_next | (*void) → *void | Next sibling with same tag name |
gx_xml_idx | (*void, c_int) → *void | Nth tag with same name (0-based) |
This is the primary pattern for iterating XML — get the first child by name,
then walk gx_xml_next until 0:
// Load raylib input bindings from XML config
// <bindings>
// <bind action="jump" key="SPACE"/>
// <bind action="shoot" key="X"/>
// </bindings>
var bind = xml.gx_xml_child(root, "bind")
while (bind != 0) {
var action = xml.gx_xml_attr(bind, "action")
var key = xml.gx_xml_attr(bind, "key")
print(" {action} -> {key}\n")
bind = xml.gx_xml_next(bind)
}
Document Order (Any Name)
| Function | Signature | Description |
|---|---|---|
gx_xml_first_child | (*void) → *void | First child (any tag name) |
gx_xml_next_sibling | (*void) → *void | Next sibling in document order |
gx_xml_parent | (*void) → *void | Parent node (0 if root) |
Use these when children have mixed tag names:
// Walk all children regardless of tag name
var child = xml.gx_xml_first_child(root)
while (child != 0) {
var tag = xml.gx_xml_name(child)
print(" <{tag}>\n")
child = xml.gx_xml_next_sibling(child)
}
Data Extraction
| Function | Signature | Description |
|---|---|---|
gx_xml_name | (*void) → cstr | Tag name |
gx_xml_txt | (*void) → cstr | Text content (empty string if none) |
gx_xml_attr | (*void, cstr) → cstr | Attribute value by name (0 if missing) |
gx_xml_is_valid | (*void) → bool | Node is non-null with a name |
gx_xml_child_count | (*void, cstr) → i64 | Count children with tag name |
gx_xml_children_count | (*void) → i64 | Count all direct children |
// Parse raylib shader metadata
// <shader name="blur" version="330">
// <uniform name="resolution" type="vec2"/>
// <uniform name="radius" type="float"/>
// </shader>
var name = xml.gx_xml_attr(shader_node, "name")
var ver = xml.gx_xml_attr(shader_node, "version")
var num_uniforms = xml.gx_xml_child_count(shader_node, "uniform")
print("Shader '{name}' (GLSL {ver}), {num_uniforms} uniforms\n")
Note: gx_xml_txt returns the text content between the opening and
closing tags. For <score>9500</score>, gx_xml_txt(node) returns "9500".
It never returns NULL — if there’s no text, it returns "".
Note: gx_xml_attr returns 0 (null) if the attribute doesn’t exist.
Always check before using the value in string interpolation.
Serialization
| Function | Signature | Description |
|---|---|---|
gx_xml_to_str | (*void) → cstr | Serialize XML tree to string |
gx_xml_free_str | (cstr) | Free string from gx_xml_to_str |
var xml_str = xml.gx_xml_to_str(root)
print("{xml_str}\n")
xml.gx_xml_free_str(xml_str)
The output is compact (no indentation or extra whitespace).
Building XML
Create new XML documents from scratch, useful for saving game state, generating config files, or exporting data.
| Function | Signature | Description |
|---|---|---|
gx_xml_new | (cstr) → *void | Create root element with tag name |
gx_xml_add_child | (*void, cstr) → *void | Add child element, returns it |
gx_xml_set_txt | (*void, cstr) | Set text content |
gx_xml_set_attr | (*void, cstr, cstr) | Set attribute (copies name+value) |
gx_xml_remove | (*void) | Remove element and free it |
// Save game state as XML
var root = xml.gx_xml_new("save")
xml.gx_xml_set_attr(root, "version", "1")
var player = xml.gx_xml_add_child(root, "player")
xml.gx_xml_set_attr(player, "name", "Hero")
xml.gx_xml_set_attr(player, "hp", "85")
xml.gx_xml_set_attr(player, "x", "320")
xml.gx_xml_set_attr(player, "y", "240")
var inv = xml.gx_xml_add_child(root, "inventory")
var item1 = xml.gx_xml_add_child(inv, "item")
xml.gx_xml_set_attr(item1, "id", "sword")
xml.gx_xml_set_attr(item1, "count", "1")
var item2 = xml.gx_xml_add_child(inv, "item")
xml.gx_xml_set_attr(item2, "id", "potion")
xml.gx_xml_set_attr(item2, "count", "5")
var xml_str = xml.gx_xml_to_str(root)
print("{xml_str}\n")
xml.gx_xml_free_str(xml_str)
xml.gx_xml_free(root)
Output:
<save version="1"><player name="Hero" hp="85" x="320" y="240"/><inventory><item id="sword" count="1"/><item id="potion" count="5"/></inventory></save>
Common Patterns
Loading a Sprite Atlas Definition
// sprites.xml:
// <atlas texture="sprites.png">
// <sprite name="player_idle" x="0" y="0" w="16" h="16"/>
// <sprite name="player_run" x="16" y="0" w="16" h="16"/>
// <sprite name="coin" x="32" y="0" w="8" h="8"/>
// </atlas>
var doc = xml.gx_xml_parse_file("assets/sprites.xml")
defer xml.gx_xml_free(doc)
var tex_path = xml.gx_xml_attr(doc, "texture")
print("Loading texture: {tex_path}\n")
var sprite = xml.gx_xml_child(doc, "sprite")
while (sprite != 0) {
var name = xml.gx_xml_attr(sprite, "name")
var sx = xml.gx_xml_attr(sprite, "x")
var sy = xml.gx_xml_attr(sprite, "y")
var sw = xml.gx_xml_attr(sprite, "w")
var sh = xml.gx_xml_attr(sprite, "h")
print(" Sprite '{name}': rect({sx},{sy},{sw},{sh})\n")
sprite = xml.gx_xml_next(sprite)
}
Checking for Missing Attributes
gx_xml_attr returns 0 when an attribute doesn’t exist:
var difficulty = xml.gx_xml_attr(config, "difficulty")
if (difficulty == 0) {
print("No difficulty set, using default\n")
}
Nested Navigation
Chain gx_xml_child calls to reach deep elements:
// <game><settings><display><resolution>1920x1080</resolution></display></settings></game>
var display = xml.gx_xml_child(xml.gx_xml_child(doc, "settings"), "display")
var res_text = xml.gx_xml_txt(xml.gx_xml_child(display, "resolution"))
print("Resolution: {res_text}\n")
Differences from JSON Module
JSON (json) | XML (xml) | |
|---|---|---|
| Library | yyjson | ezxml |
| Data model | Values (null, bool, number, string, array, object) | Elements with attributes and text |
| Separate doc/root | Yes — gx_json_parse returns doc, gx_json_root returns root | No — gx_xml_parse returns root directly |
| Null checks | if (val != 0) | if (node != 0) |
| Immutable/mutable | Separate APIs | Single API (parsed trees are writable) |
Platform Notes
- Windows: Compiled with
-DEZXML_NOMMAP(nommap). Uses_open/_close/_readfor file I/O. - Linux/macOS: Uses
mmapfor efficient file parsing when available. - TCC: Fully supported (ezxml is simple C with no exotic dependencies).
Module Layout
modules/
xml/
c/
ezxml.c ezxml source (compiled via @cfile)
ezxml.h ezxml header
gx_xml.h GX helper wrappers (void* interface)
gx/
xml.gx Module declarations, extern functions