Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Embedding sxml in C

Everything the sxml command does, it does through the sxml C library, and your programs can use the same library. It is portable C17, has no dependencies, never calls malloc behind your back, keeps no global state, and never recurses, so a deeply nested document cannot overflow your program's stack. This chapter shows the shape of the API with five small programs, most of them on the Vane draft. The examples are in docs/book/examples/c, where vane.xml and vane-00.xml are links to the drafts in docs/book/examples/spec.

Building against sxml

The simplest way to use sxml is to build it as part of your project with CMake. Put the sxml source in your tree (as a git submodule, for example) and add it as a subdirectory:

CMakeLists.txt:

cmake_minimum_required(VERSION 3.24)
project(sxml_book_examples C)

# Where the sxml source is: a checkout, a git submodule, or a directory
# you unpacked. Only the library is built, without sxml's own tests and
# command.
set(SXML_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/sxml" CACHE PATH "The sxml source directory")
set(SXML_BUILD_TESTS OFF)
set(SXML_BUILD_APPS OFF)
add_subdirectory("${SXML_SOURCE}" sxml EXCLUDE_FROM_ALL)

foreach(example requirements compile_once resolver errors memory)
    add_executable(${example} ${example}.c)
    target_link_libraries(${example} PRIVATE sxml)
endforeach()

SXML_BUILD_TESTS and SXML_BUILD_APPS leave out sxml's own tests and the command, and the sxml target is the static library with every header in include/sxml/. A program links only what it uses: one that uses only XPath doesn't link XQuery, XSLT or XSD.

The examples' CMakeLists.txt takes the location of the sxml source as SXML_SOURCE. Here $SXML is the root of an sxml checkout:

$ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DSXML_SOURCE="$SXML" > /dev/null
$ cmake --build build > /dev/null

The examples share a few lines of input and output code, in output.h:

/* Small helpers that the book's C examples share. */
#pragma once

#include <stdio.h>

#include "sxml/processor.h"
#include "sxml/serialize.h"

#define TEXT(literal) SXML_BYTE_VIEW_FROM_LITERAL(literal)

/* Reads the file at path into contents, which holds capacity bytes, and
   returns a view of what it read: empty when the file cannot be read or
   does not fit. */
static inline sxml_byte_view read_file(const char *path, unsigned char *contents,
                                       size_t capacity) {
    sxml_byte_view bytes = {.bytes = contents, .byte_count = 0};
    FILE *file = fopen(path, "rb");
    if (file != NULL) {
        bytes.byte_count = fread(contents, 1, capacity, file);
        if (!feof(file)) {
            bytes.byte_count = 0;
        }
        fclose(file);
    }
    return bytes;
}

/* Serialized bytes go to an output stream; this one writes to stdout. */
static sxml_status write_to_stdout(void *state, sxml_byte_view bytes) {
    (void)state;
    size_t written = fwrite(bytes.bytes, 1, bytes.byte_count, stdout);
    return written == bytes.byte_count ? SXML_STATUS_OK
                                       : SXML_STATUS_INPUT_OUTPUT_FAILED;
}

static const sxml_output_stream_operations stdout_operations = {
    .write = write_to_stdout,
};

static inline sxml_output_stream stdout_stream(void) {
    return (sxml_output_stream){.operations = &stdout_operations};
}

/* Writes a sequence by the adaptive method, one item on each line. */
static inline sxml_status print_sequence(sxml_processor *processor,
                                         sxml_sequence sequence,
                                         sxml_error_record *error_record) {
    sxml_status status = sxml_serialize_sequence(
        sxml_processor_get_item_heap(processor),
        &(sxml_serialization_request){
            .value = sequence,
            .serialization_parameters =
                {.serialization_method = SXML_SERIALIZATION_METHOD_ADAPTIVE},
            .error_record = error_record,
        },
        stdout_stream());
    putchar('\n');
    return status;
}

/* Writes what went wrong: the status, and the error record when there is
   one. */
static inline void print_failure(sxml_status status,
                                 const sxml_error_record *error_record) {
    sxml_byte_view status_name = sxml_status_name(status);
    sxml_byte_view code = error_record->code.local_name;
    sxml_byte_view description = error_record->diagnostic.description;
    sxml_source_location location = error_record->diagnostic.location;
    printf("%.*s", (int)status_name.byte_count, (const char *)status_name.bytes);
    if (code.byte_count > 0) {
        printf(" err:%.*s", (int)code.byte_count, (const char *)code.bytes);
    }
    if (location.line_number > 0) {
        printf(" at %u:%u", (unsigned)location.line_number,
               (unsigned)location.column_number);
    }
    if (description.byte_count > 0) {
        printf(": %.*s", (int)description.byte_count, (const char *)description.bytes);
    }
    putchar('\n');
}

sxml_byte_view is how the API passes text: a pointer and a length, never a NUL-terminated string. sxml reads no files, so read_file reads a draft into memory the program owns and returns a view of its bytes. Output goes to an sxml_output_stream, an interface you implement; this one writes to stdout.

A first program

The first program prints the draft's MUST requirements and the section each one is in:

requirements.c:

#include <stdio.h>

#include "output.h"
#include "sxml/processor.h"
#include "sxml/xpath.h"

/* Room for the draft's bytes. */
static unsigned char draft_contents[1 << 16];

int main(void) {
    sxml_allocator allocator = sxml_default_allocator();
    sxml_error_record error_record = {0};

    /* 1. A processor owns names, types and values. */
    sxml_processor *processor = NULL;
    sxml_status status = sxml_processor_create(
        allocator, &(sxml_processor_configuration){.allocator = allocator},
        &processor);

    /* 2. Parse a document: the program reads the file, sxml parses it. */
    sxml_item document = {0};
    if (status == SXML_STATUS_OK) {
        status = sxml_processor_parse_document(
            processor,
            &(sxml_xml_parse_request){
                .input = read_file("vane.xml", draft_contents, sizeof draft_contents),
                .diagnostic = &error_record.diagnostic,
            },
            &document);
    }
    sxml_sequence context = sxml_sequence_from_item(document);

    /* 3. Compile an expression. */
    sxml_xpath_expression *expression = NULL;
    if (status == SXML_STATUS_OK) {
        status = sxml_xpath_expression_create(
            allocator,
            &(sxml_xpath_expression_configuration){
                .processor = processor,
                .expression_text = TEXT("//t[bcp14 = 'MUST'] ! `{@anchor} in {ancestor::section[1]/name}`"),
                .error_record = &error_record,
            },
            &expression);
    }

    /* 4. Evaluate it, with the document as the context item. */
    sxml_sequence result = sxml_sequence_empty();
    if (status == SXML_STATUS_OK) {
        status = sxml_xpath_expression_evaluate(
            expression,
            &(sxml_xpath_evaluation_request){
                .dynamic_context = {.has_context_value = true,
                                    .context_value = context},
                .error_record = &error_record,
            },
            &result);
    }

    /* 5. Write the result. */
    if (status == SXML_STATUS_OK) {
        status = print_sequence(processor, result, &error_record);
    }
    if (status != SXML_STATUS_OK) {
        print_failure(status, &error_record);
    }

    /* 6. Release everything, the processor last. */
    sxml_sequence_release(result);
    sxml_xpath_expression_destroy(expression);
    sxml_sequence_release(context);
    sxml_processor_destroy(processor);
    return status == SXML_STATUS_OK ? 0 : 1;
}
$ ./build/requirements
"req-1 in Flag States"
"req-2 in Flag States"
"req-4 in Rollout"
"req-6 in Status"
"req-7 in Pause"

The steps are the same in every program:

  1. Create a processor. It owns the names, types and values that everything else uses, and it holds what the host provides: the resource resolver, the clock, the trace sink and the limits.
  2. Parse documents with the processor, from bytes you give it.
  3. Compile an expression, a query or a stylesheet.
  4. Evaluate it, with a dynamic context: the context item and the values of variables.
  5. Serialize the result, or read its items with the functions of sxml/xdm.h.
  6. Release values, destroy compiled objects, then the processor.

Every function returns an sxml_status. Arguments come in structs with designated initializers, so a call says what each value is, and fields you leave out are zero: the defaults.

Compile once, run many times

Compiling parses the expression, resolves its names, checks its types and optimizes it. Do it once, and evaluate the result as often as you like, with different documents and different variables. This program compiles one expression and runs it on both drafts, once for each of two key words:

compile_once.c:

#include <stdio.h>

#include "output.h"
#include "sxml/processor.h"
#include "sxml/xpath.h"

/* The two drafts the same expression runs against, and room for each. */
static const char *const draft_files[] = {"vane-00.xml", "vane.xml"};
static unsigned char draft_contents[2][1 << 16];

/* The values of $keyword for each run. */
static const sxml_byte_view keywords[] = {TEXT("SHOULD"), TEXT("MAY")};

int main(void) {
    sxml_allocator allocator = sxml_default_allocator();
    sxml_error_record error_record = {0};
    sxml_processor *processor = NULL;
    sxml_status status = sxml_processor_create(
        allocator, &(sxml_processor_configuration){.allocator = allocator},
        &processor);

    /* Variable names are interned in the processor's name pool. */
    sxml_name_identifier keyword = {0};
    if (status == SXML_STATUS_OK) {
        status = sxml_name_pool_intern_name(
            sxml_processor_get_name_pool(processor),
            &(sxml_name_request){.namespace_uri = SXML_NAMESPACE_NONE,
                                 .local_name = TEXT("keyword")},
            &keyword);
    }

    /* Compile once, declaring $keyword as an external xs:string. */
    sxml_variable_declaration declaration = {
        .variable_name = keyword,
        .declared_type = {.item_type = {.index = SXML_TYPE_XS_STRING},
                          .occurrence = SXML_OCCURRENCE_EXACTLY_ONE},
    };
    sxml_xpath_expression *expression = NULL;
    if (status == SXML_STATUS_OK) {
        status = sxml_xpath_expression_create(
            allocator,
            &(sxml_xpath_expression_configuration){
                .processor = processor,
                .expression_text = TEXT("string-join(//t[bcp14 = $keyword]/@anchor, ' ')"),
                .static_context = {.variable_declarations = {
                                       .variable_declarations = &declaration,
                                       .variable_declaration_count = 1}},
                .error_record = &error_record,
            },
            &expression);
    }

    /* Run many times: each draft, with each key word. */
    for (size_t draft_index = 0; status == SXML_STATUS_OK && draft_index < 2;
         draft_index += 1) {
        sxml_item document = {0};
        status = sxml_processor_parse_document(
            processor,
            &(sxml_xml_parse_request){
                .input = read_file(draft_files[draft_index], draft_contents[draft_index],
                                   sizeof draft_contents[draft_index]),
                .diagnostic = &error_record.diagnostic},
            &document);
        sxml_sequence context = sxml_sequence_from_item(document);
        for (size_t keyword_index = 0; status == SXML_STATUS_OK && keyword_index < 2;
             keyword_index += 1) {
            /* A string is an item in the processor's item heap. */
            sxml_item value = {0};
            status = sxml_atomic_item_make(
                sxml_processor_get_item_heap(processor),
                &(sxml_atomic_item_request){
                    .type = sxml_builtin_type_identifier(SXML_TYPE_XS_STRING),
                    .datum = {.datum_kind = SXML_DATUM_KIND_STRING,
                              .datum_details = {.string = {.utf8 = keywords[keyword_index]}}},
                },
                &value);
            sxml_variable_binding binding = {
                .variable_name = keyword,
                .value = sxml_sequence_from_item(value),
            };
            sxml_sequence result = sxml_sequence_empty();
            if (status == SXML_STATUS_OK) {
                status = sxml_xpath_expression_evaluate(
                    expression,
                    &(sxml_xpath_evaluation_request){
                        .dynamic_context = {.has_context_value = true,
                                            .context_value = context,
                                            .variable_bindings = {
                                                .variable_bindings = &binding,
                                                .variable_binding_count = 1}},
                        .error_record = &error_record,
                    },
                    &result);
            }
            if (status == SXML_STATUS_OK) {
                printf("%s, %.*s: ", draft_files[draft_index],
                       (int)keywords[keyword_index].byte_count,
                       (const char *)keywords[keyword_index].bytes);
                status = print_sequence(processor, result, &error_record);
            }
            sxml_sequence_release(result);
            sxml_sequence_release(binding.value);
        }
        sxml_sequence_release(context);
    }

    if (status != SXML_STATUS_OK) {
        print_failure(status, &error_record);
    }
    sxml_xpath_expression_destroy(expression);
    sxml_processor_destroy(processor);
    return status == SXML_STATUS_OK ? 0 : 1;
}
$ ./build/compile_once
vane-00.xml, SHOULD: "req-3 req-8"
vane-00.xml, MAY: ""
vane.xml, SHOULD: "req-3 req-8"
vane.xml, MAY: "req-9 req-11"

The first draft has no MAY requirement; the second adds two.

An external variable is declared when the expression is compiled, with its type, and bound each time it is evaluated. Names are interned once in the processor's name pool and then passed as small identifiers. Small integers, doubles and booleans are items that need no memory; strings and other values are made in the processor's item heap, as sxml_atomic_item_make makes each key word, and released when they are no longer needed.

Resources and XQuery

The processor's resource resolver answers every request for a resource: fn:doc, fn:unparsed-text, imported modules, schemas, packages. sxml reads nothing else. sxml_table_resource_resolver serves resources from memory, which is all a test or a sandbox needs; for anything else, implement sxml_resource_resolver_operations yourself.

This program also shows XQuery. A query is compiled and evaluated like an expression, and it carries the serialization parameters its prolog declares. The program reads the draft into memory and serves it as https://example.org/drafts/vane.xml, and the query counts the requirements of each key word:

resolver.c:

#include <stdio.h>

#include "output.h"
#include "sxml/processor.h"
#include "sxml/xquery.h"

/* Room for the draft's bytes. */
static unsigned char draft_contents[1 << 16];

int main(void) {
    sxml_allocator allocator = sxml_default_allocator();
    sxml_error_record error_record = {0};

    /* The only resource the query may read: the draft, held in memory. */
    sxml_resource_entry resources[] = {
        {.uri = TEXT("https://example.org/drafts/vane.xml"),
         .content = read_file("vane.xml", draft_contents, sizeof draft_contents)},
    };
    sxml_resource_entry_view entries = {.resource_entries = resources,
                                        .resource_entry_count = 1};

    /* The resolver decides what fn:doc can see; nothing else is read. */
    sxml_processor *processor = NULL;
    sxml_status status = sxml_processor_create(
        allocator,
        &(sxml_processor_configuration){
            .allocator = allocator,
            .resource_resolver = sxml_table_resource_resolver(&entries),
        },
        &processor);

    /* Relative URIs in the query resolve against its static base URI. */
    sxml_xquery_query *query = NULL;
    if (status == SXML_STATUS_OK) {
        status = sxml_xquery_query_create(
            allocator,
            &(sxml_xquery_query_configuration){
                .processor = processor,
                .expression_text = TEXT(
                    "declare option output:indent 'yes';\n"
                    "<keywords>{\n"
                    "  for $t in doc('vane.xml')//t[@anchor]\n"
                    "  group by $keyword := string($t/bcp14)\n"
                    "  order by $keyword\n"
                    "  return <keyword name='{ $keyword }' count='{ count($t) }'/>\n"
                    "}</keywords>,\n"
                    "doc-available('vane-00.xml')"),
                .source_name = TEXT("keywords.xq"),
                .static_context = {.static_base_uri = TEXT("https://example.org/drafts/")},
                .error_record = &error_record,
            },
            &query);
    }

    sxml_sequence result = sxml_sequence_empty();
    if (status == SXML_STATUS_OK) {
        status = sxml_xquery_query_evaluate(
            query, &(sxml_xquery_evaluation_request){.error_record = &error_record},
            &result);
    }

    /* Serialize as the query's output declarations ask. */
    if (status == SXML_STATUS_OK) {
        status = sxml_serialize_sequence(
            sxml_processor_get_item_heap(processor),
            &(sxml_serialization_request){
                .value = result,
                .serialization_parameters =
                    sxml_xquery_query_get_serialization_parameters(query),
                .error_record = &error_record,
            },
            stdout_stream());
        putchar('\n');
    }
    if (status != SXML_STATUS_OK) {
        print_failure(status, &error_record);
    }
    sxml_sequence_release(result);
    sxml_xquery_query_destroy(query);
    sxml_processor_destroy(processor);
    return status == SXML_STATUS_OK ? 0 : 1;
}
$ ./build/resolver
<keywords>
  <keyword name="MAY" count="2"/>
  <keyword name="MUST" count="5"/>
  <keyword name="MUST NOT" count="1"/>
  <keyword name="SHOULD" count="2"/>
  <keyword name="SHOULD NOT" count="1"/>
</keywords>false

doc('vane.xml') resolves against the query's base URI to the URI in the table. vane-00.xml is not in the table, so the query cannot see it, though the file is right next to the program: the resolver decides, not the file system. XSLT (sxml/xslt.h) and XSD (sxml/schema.h) follow the same pattern: compile a stylesheet or a schema once with a processor, then run transformations or validations with it.

Errors

When a function fails, its status says how, and the error record you passed says what and where: the error code, the description and the location in the expression text. Static errors come from compiling, dynamic and type errors from evaluating:

errors.c:

#include <stdio.h>

#include "output.h"
#include "sxml/processor.h"
#include "sxml/xpath.h"

/* Compiles and evaluates one expression, and reports how it went. */
static void run(sxml_processor *processor, sxml_byte_view expression_text) {
    sxml_allocator allocator = sxml_default_allocator();
    sxml_error_record error_record = {0};
    sxml_xpath_expression *expression = NULL;
    sxml_sequence result = sxml_sequence_empty();
    printf("%.*s => ", (int)expression_text.byte_count,
           (const char *)expression_text.bytes);
    sxml_status status = sxml_xpath_expression_create(
        allocator,
        &(sxml_xpath_expression_configuration){
            .processor = processor,
            .expression_text = expression_text,
            .error_record = &error_record,
        },
        &expression);
    if (status == SXML_STATUS_OK) {
        status = sxml_xpath_expression_evaluate(
            expression, &(sxml_xpath_evaluation_request){.error_record = &error_record},
            &result);
    }
    if (status == SXML_STATUS_OK) {
        status = print_sequence(processor, result, &error_record);
    }
    if (status != SXML_STATUS_OK) {
        print_failure(status, &error_record);
    }
    sxml_sequence_release(result);
    sxml_xpath_expression_destroy(expression);
}

int main(void) {
    sxml_allocator allocator = sxml_default_allocator();
    sxml_processor *processor = NULL;
    sxml_status status = sxml_processor_create(
        allocator,
        &(sxml_processor_configuration){
            .allocator = allocator,
            /* Stop any evaluation after ten million steps. */
            .limits = {.maximum_evaluation_step_count = 10000000},
        },
        &processor);
    if (status != SXML_STATUS_OK) {
        return 1;
    }
    run(processor, TEXT("6 * 7"));
    run(processor, TEXT("1 +"));
    run(processor, TEXT("1 div 0"));
    run(processor, TEXT("error(xs:QName('err:VANE0001'), 'no requirement req-12')"));
    run(processor, TEXT("some $n in 1 to 100000000000 satisfies $n = 0"));
    sxml_processor_destroy(processor);
    return 0;
}
$ ./build/errors
6 * 7 => 42
1 + => SXML_STATUS_STATIC_ERROR err:XPST0003 at 1:3: The text is not valid XPath 4.0 syntax (XPath 4.0, Appendix A)
1 div 0 => SXML_STATUS_DYNAMIC_ERROR err:FOAR0001 at 1:1: division by zero (F&O 4.0, op:numeric-divide, op:numeric-integer-divide, op:numeric-mod)
error(xs:QName('err:VANE0001'), 'no requirement req-12') => SXML_STATUS_DYNAMIC_ERROR err:VANE0001 at 1:1: no requirement req-12
some $n in 1 to 100000000000 satisfies $n = 0 => SXML_STATUS_LIMIT_EXCEEDED: the evaluation took more steps than maximum_evaluation_step_count allows

The last expression would run for a very long time. The processor's limits bound the work of an evaluation, the depth of calls, the size of documents and the memory of the item heap, so a program can run queries it doesn't trust. A limit gives SXML_STATUS_LIMIT_EXCEEDED, not an error code: it is not an error in the query, but a decision of the host.

Memory and allocators

sxml takes all its memory from an sxml_allocator that you give it: a pair of functions to allocate and release, with a state pointer. The default allocator uses the C library's heap. Yours can use a pool, an arena, a fixed budget, or memory that a game engine or a kernel hands out. The counting allocator that comes with sxml counts the allocations, which makes it easy to check that nothing leaks:

memory.c:

#include <stdio.h>

#include "output.h"
#include "sxml/processor.h"
#include "sxml/xpath.h"

int main(void) {
    /* A counting allocator passes allocations on to the default
       allocator and counts them. Any sxml_allocator works the same way:
       a pool, an arena, or memory with a fixed budget. */
    sxml_counting_allocator *counting_allocator = NULL;
    sxml_status status = sxml_counting_allocator_create(
        sxml_default_allocator(),
        &(sxml_counting_allocator_configuration){
            .backing_allocator = sxml_default_allocator()},
        &counting_allocator);
    if (status != SXML_STATUS_OK) {
        return 1;
    }
    sxml_allocator allocator = sxml_counting_allocator_as_allocator(counting_allocator);
    sxml_error_record error_record = {0};

    sxml_processor *processor = NULL;
    status = sxml_processor_create(
        allocator, &(sxml_processor_configuration){.allocator = allocator},
        &processor);
    sxml_xpath_expression *expression = NULL;
    if (status == SXML_STATUS_OK) {
        status = sxml_xpath_expression_create(
            allocator,
            &(sxml_xpath_expression_configuration){
                .processor = processor,
                .expression_text = TEXT("sum((1 to 1000000) ! (. * .))"),
                .error_record = &error_record,
            },
            &expression);
    }
    sxml_sequence result = sxml_sequence_empty();
    if (status == SXML_STATUS_OK) {
        status = sxml_xpath_expression_evaluate(
            expression, &(sxml_xpath_evaluation_request){.error_record = &error_record},
            &result);
    }
    if (status == SXML_STATUS_OK) {
        status = print_sequence(processor, result, &error_record);
    }
    if (status != SXML_STATUS_OK) {
        print_failure(status, &error_record);
    }
    sxml_sequence_release(result);
    sxml_xpath_expression_destroy(expression);
    sxml_processor_destroy(processor);

    /* Everything sxml allocated has been given back. */
    sxml_allocation_counts counts = sxml_counting_allocator_read_counts(counting_allocator);
    printf("allocations: %llu, still live: %llu\n",
           (unsigned long long)counts.attempted_allocation_count,
           (unsigned long long)counts.live_allocation_count);
    sxml_counting_allocator_destroy(counting_allocator);
    return status == SXML_STATUS_OK ? 0 : 1;
}
$ ./build/memory
333333833333500000
allocations: 195, still live: 0

A million squares took 195 allocations, because sequences such as 1 to 1000000 are computed as they are read, not stored.

Every object type in sxml can also be placed in memory you provide, without an allocator call for the object itself: each has a memory_requirements function that says how much memory and what alignment it needs, an initialize function that places it, and a finalize function that ends it. create and destroy, used above, are the same with the memory from an allocator.

Optional features

Some parts of sxml are optional features: the HTML parser, Invisible XML, EXPath Binary and File, UCA collations, the time zone database, localized formatting and Full Text. A program names the features it wants in the processor configuration:

sxml_optional_feature features[] = {
    sxml_html_parser_feature(),
    sxml_invisible_xml_feature(),
};
sxml_processor_configuration configuration = {
    .allocator = allocator,
    .optional_features = {.optional_features = features,
                          .optional_feature_count = 2},
};

A program that never names a feature doesn't link it, which keeps small programs small. sxml_all_optional_features() names them all, except EXPath File: a feature that reads and writes files beyond the resolver must be named on purpose, with an sxml_file_system that decides which files it reaches.

Without a feature, sxml behaves as the specifications say a processor without it behaves: parse-html doesn't exist, for example. The large tables some features need, such as collation data and time zones, are not compiled in. The feature asks the resolver for them, in their standard formats, the first time they are needed.

Threads

A processor and everything made with it are used by one thread at a time. Separate processors share nothing, so a server runs one processor per thread, with no locks.

Where to go next

The public headers in include/sxml/ are the reference. Each one documents its types and functions, with the rules of the specification they follow. Start with processor.h and xpath.h, then xquery.h, xslt.h and schema.h.