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

FunctionSignatureDescription
gx_xml_parse(cstr, i64) → *voidParse XML string with length
gx_xml_parse_file(cstr) → *voidParse 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

FunctionSignatureDescription
gx_xml_error(*void) → cstrError message (empty string if none)
gx_xml_has_error(*void) → boolTrue 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>.

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

FunctionSignatureDescription
gx_xml_child(*void, cstr) → *voidFirst child with given tag name
gx_xml_next(*void) → *voidNext sibling with same tag name
gx_xml_idx(*void, c_int) → *voidNth 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)

FunctionSignatureDescription
gx_xml_first_child(*void) → *voidFirst child (any tag name)
gx_xml_next_sibling(*void) → *voidNext sibling in document order
gx_xml_parent(*void) → *voidParent 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

FunctionSignatureDescription
gx_xml_name(*void) → cstrTag name
gx_xml_txt(*void) → cstrText content (empty string if none)
gx_xml_attr(*void, cstr) → cstrAttribute value by name (0 if missing)
gx_xml_is_valid(*void) → boolNode is non-null with a name
gx_xml_child_count(*void, cstr) → i64Count children with tag name
gx_xml_children_count(*void) → i64Count 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

FunctionSignatureDescription
gx_xml_to_str(*void) → cstrSerialize 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.

FunctionSignatureDescription
gx_xml_new(cstr) → *voidCreate root element with tag name
gx_xml_add_child(*void, cstr) → *voidAdd 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)
Libraryyyjsonezxml
Data modelValues (null, bool, number, string, array, object)Elements with attributes and text
Separate doc/rootYes — gx_json_parse returns doc, gx_json_root returns rootNo — gx_xml_parse returns root directly
Null checksif (val != 0)if (node != 0)
Immutable/mutableSeparate APIsSingle API (parsed trees are writable)

Platform Notes

  • Windows: Compiled with -DEZXML_NOMMAP (no mmap). Uses _open/_close/_read for file I/O.
  • Linux/macOS: Uses mmap for 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