-
XML 1.0 parsing with UTF-8 validation and optional encoding conversion (UTF-16, ISO-8859, Shift-JIS, EBCDIC, and others via iconv). Zero-copy text nodes — borrowed views into the input buffer, no per-node content copy (TODO 115).
-
XPath 1.0 engine implementing all 13 axes, 27 functions, and 15 operators. Bytecode VM for compile-once-eval-many dispatch (TODO 120).
-
Streaming SAX parser with an explicit state machine — events emit as chunks arrive, memory bounded by nesting depth, not document size (TODO 116). All SAX parsing routes through one state machine; the legacy recursive parser has been removed (~840 lines deleted).
-
XInclude 1.0 with ownership-transfer splice — included documents are moved (not deep-copied) into the parent tree. Cycle detection via ancestor-URI tracking (TODO 117).
-
DTD validation with content-model memoization — repeated element types with the same children signature skip the matcher on subsequent calls (TODO 119).
-
Canonical XML (C14N) for digital signatures and cryptographic hashing.
-
Pool-based memory model — every allocation reachable from a document is released in a single
taurus_document_freecall. Zero leaks across the test suite. -
Compact-pointer architecture — tree edges stored as int32_t byte offsets with overflow-table fallback for macOS ASLR (TODO 121).
-
Recursion depth guard — deeply nested input is rejected with a parse error rather than crashing.
-
Per-document strict mode — strict and lenient parsing can coexist in the same thread.
-
Vtable-based dispatch — adding a new node type is purely additive; no switches to edit.
-
CLI tool —
taurus parse,taurus xpath,taurus format,taurus versionfor command-line XML processing. -
Ruby FFI binding —
Taurus::Document.parse, XPath, serialize via theffigem. No C extension compilation needed. See Ruby binding. -
Zero required runtime dependencies — utf8proc and iconv are optional features, not prerequisites.
-
Stable C ABI with a documented FFI contract; bindings shipped for Ruby (Python and Rust planned). See FFI Design.
| Use libtaurus when | Consider alternatives when |
|---|---|
You need a C library with a small footprint and no required runtime dependencies. |
You need full XML Schema 1.1 validation. |
You parse documents that may not fit in memory (SAX streaming). |
You need XSLT 1.0 / 2.0 transformation. |
You need deterministic memory usage and zero leaks in normal operation. |
You need XQuery 1.0 (XPath only here). |
You want to embed XML processing in another language via FFI. |
You’re already on a platform with libxml2 + bindings you trust. |
==
#include <taurus.h>
#include <stdio.h>
#include <string.h>
int main(void) {
const char* xml = "<root><item>hello</item></root>";
TaurusStatus status = TAURUS_OK;
TaurusDocument doc = taurus_parse_string(xml, strlen(xml), &status);
if (!doc) {
fprintf(stderr, "parse failed: %d\n", status);
return 1;
}
TaurusElement root = taurus_document_root(doc);
printf("root element: %s\n", taurus_element_name(root));
TaurusXPathResult items = taurus_xpath_eval(doc, NULL, "//item");
printf("item count: %zu\n", taurus_xpath_result_count(items));
taurus_xpath_result_free(items);
taurus_document_free(doc); /* releases the entire pool */
return 0;
}Compile and run:
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/cli/taurus parse 'fixtures/basic.xml'
./build/cli/taurus xpath 'fixtures/basic.xml' 'count(//item)'cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build
sudo cmake --install build --prefix /usr/localAfter install, the library is discoverable via find_package(taurus):
cmake_minimum_required(VERSION 3.20)
project(myapp LANGUAGES C CXX)
find_package(taurus CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE taurus::taurus)Or via pkg-config:
gcc myapp.c $(pkg-config --cflags --libs taurus)git clone https://github.com/microsoft/vcpkg
./vcpkg/vcpkg install taurusSee vcpkg integration for the portfile template.
| Distro | Install command |
|---|---|
Alpine |
|
Debian/Ubuntu |
|
Homebrew |
|
MSYS2 (Windows) |
|
| Option | Default | Description |
|---|---|---|
|
|
Build the Google Test suite under |
|
|
Build the |
|
|
Build performance comparison targets (libxml2 / pugixml). |
|
|
Generate man pages from the AsciiDoc sources. |
|
|
UTF-8 validation via utf8proc. |
|
|
Encoding conversion via iconv (ISO-8859-1, Shift-JIS, etc.). |
|
|
Build with AddressSanitizer. |
|
|
Build the libFuzzer harness. |
|
|
Generate Doxygen API docs. |
cmake -B build-asan -S . -DTAURUS_ENABLE_ASAN=ON -DBUILD_TESTING=ON
cmake --build build-asan
ASAN_OPTIONS=detect_leaks=1 ctest --test-dir build-asanbrew install llvm # macOS
export CC=/opt/homebrew/opt/llvm/bin/clang
cmake -B build-fuzz -S . -DTAURUS_ENABLE_FUZZING=ON
cmake --build build-fuzz --target fuzz_parse
./build-fuzz/fuzz_parse -max_total_time=600 corpus/brew install doxygen
cmake -B build -S . -DTAURUS_BUILD_DOCS=ON
cmake --build build --target docs
open build/docs/api-generated/html/index.htmlThe library exposes a single import target:
target_link_libraries(your_app PRIVATE taurus::taurus)taurus_dep = dependency('taurus')
executable('your_app', 'main.c', dependencies: taurus_dep)taurus_parse_string is the entry point. It accepts a UTF-8 buffer
and a status output parameter:
TaurusStatus status;
TaurusDocument doc = taurus_parse_string(xml, strlen(xml), &status);
if (!doc) {
/* status is one of TAURUS_ERROR_PARSE, TAURUS_ERROR_MEMORY, ... */
}
/* Document is now a pool of nodes; no need to track them individually. */
/* Always release the document — the pool is destroyed too. */
taurus_document_free(doc);TaurusXPathResult r = taurus_xpath_eval(doc, NULL, "//item[@price > 10]");
if (r) {
size_t n = taurus_xpath_result_count(r);
for (size_t i = 0; i < n; i++) {
TaurusNodeRef node = taurus_xpath_result_node(r, i);
printf(" %s\n", taurus_node_name(node));
}
taurus_xpath_result_free(r);
}Supported: all 13 axes, all 27 functions, all 15 operators, full predicate syntax. See xpath-coverage for details.
static void on_start(void* ud, const char* name, const char** attrs) {
fprintf(stderr, "<%s>\n", name);
}
TaurusSAXHandler handler = {0};
handler.start_element = on_start;
taurus_sax_parse(xml, len, &handler, NULL);TaurusSerializeOptions opts = { .indent = 2, .xml_declaration = 1 };
char* out = taurus_document_serialize(doc, &opts);
puts(out);
taurus_free_string(out);char* canonical = taurus_c14n_canonicalize(doc, TAURUS_C14N_1_0, 0);
fputs(canonical, stdout);
putchar('\n'); /* canonical output may not end with newline */
taurus_free_string(canonical);
taurus_free_string(canonical);Every byte the parser allocates that ends up referenced by a document
lives in the document’s pool. taurus_document_free destroys the
pool and releases everything in one call.
| Allocation | Where it lives |
|---|---|
Node structs (element, text, comment, CDATA, PI, doctype) |
Pool, allocated contiguously with content where possible. |
Node content strings |
Pool, contiguous with the struct (cache locality). |
Attribute names |
Pool hash table (interned; dedup across elements). |
Attribute values |
Pool, bypassing interning (attrs.xml regression fixed). |
DTD container + hash tables |
Pool, with DTD subsystem owned by the document. |
XPath intermediates |
Pool, freed at result destruction. |
For bindings: the C API has opaque handles. All freeing is
explicit. See the Memory: comment on each public function.
Opaque handles are pointer-sized — enforced at compile time:
_Static_assert(sizeof(TaurusDocument) == sizeof(void*), "...");To pin enum values (bindings hard-code these):
ctest --test-dir build -R HeaderHygienelibtaurus exposes a stable C ABI. Bindings:
-
Ruby — shipped. Uses
ffigem. See Ruby binding. -
Python — planned via
cffi(header-aware) -
Rust — planned via
bindgen+ idiomatic wrapper
require 'taurus'
doc = Taurus::Document.parse('<root><item id="1">hello</item></root>')
root = doc.root
puts root.name # => "root"
puts root.first_child_element['id'] # => "1"
puts doc.xpath('count(//item)') # => 1.0
doc.freeInstall: set TAURUS_LIB_PATH to your libtaurus.dylib / .so, or
install libtaurus system-wide. The ffi gem is the only dependency.
gem install ffi
TAURUS_LIB_PATH=/path/to/libtaurus.0.dylib ruby -Ilib -rtaurus -e '
doc = Taurus::Document.parse("<r/>")
puts doc.root.name
doc.free
'To parse the headers from a binding tool:
cc -DTAURUS_FOR_BINDGEN -E src/include/taurus.h # strips TAURUS_APISee docs/FFI.md for the full design document.
# Parse a document
taurus parse document.xml
# Round-trip via XPath count
taurus xpath --count document.xml 'count(//item)'
# Pretty-print
taurus format --indent 4 document.xml < ugly.xml > pretty.xml
# Validate / version
taurus versionExit codes: 0 on success, 1 on parse error or invalid usage.
Taurus is benchmarked against libxml2 and pugixml on every push via CI
(GitHub Actions, Linux + macOS). Numbers below are from Apple Silicon,
clang -O3 -flto=thin (LTO is default for Release builds since TODO 110).
| Benchmark | Taurus | libxml2 | Advantage |
|---|---|---|---|
SAX small (~1 KB) |
2.8 µs (377 MB/s) |
7.1 µs (124 MB/s) |
2.5× faster |
SAX medium (~5 KB) |
7.5 µs (624 MB/s) |
26.9 µs (175 MB/s) |
3.6× faster |
DOM parse (~5 KB) |
33 µs |
47 µs |
1.4× faster |
| Benchmark | Taurus | libxml2 | Advantage |
|---|---|---|---|
Attribute lookup by name |
1.6 µs |
3.0 µs |
1.9× faster |
Text content extraction |
1.4 µs |
3.5 µs |
2.6× faster |
Indexed child access (1000 × 50) |
2.3 µs (O(1) cached) |
2.5 µs |
9% faster |
Taurus’s XPath engine uses a bytecode VM with per-axis
specialization, predicate fast paths, absolute-path fusion, a
per-document element index with attribute buckets, fused
axis+predicate opcodes, and memcpy fast paths for index-backed
queries (TODO 120, TODO 125–137). Numbers below are from
benchmarks/xpath/bench_diagnostic on a ~5 KB catalog fixture,
CPU time.
| Benchmark | Taurus | libxml2 | Advantage |
|---|---|---|---|
|
0.57 µs |
0.89 µs |
1.6× faster |
|
0.71 µs |
0.94 µs |
1.3× faster |
|
0.63 µs |
2.52 µs |
4.0× faster |
|
0.72 µs |
0.96 µs |
1.3× faster |
|
0.74 µs |
0.99 µs |
1.3× faster |
|
0.55 µs |
~1 µs |
1.8× faster |
|
0.56 µs |
~1 µs |
1.8× faster |
|
1.13 µs |
~3 µs |
2.7× faster |
|
0.77 µs |
1.02 µs |
1.3× faster |
|
0.53 µs |
~1 µs |
1.9× faster |
Taurus BEATS libxml2 on all 10 XPath benchmarks.
| Benchmark | Taurus | pugixml | libxml2 |
|---|---|---|---|
Append 1000 children |
15.0 µs |
11.9 µs |
56.2 µs |
Set 100 attributes |
43.3 µs |
10.4 µs |
33.4 µs |
Set text |
0.9 µs |
0.7 µs |
0.8 µs |
Parse + 10 writes (medium) |
30.7 µs |
5.1 µs |
41.6 µs |
Taurus beats libxml2 on every benchmark except set-text (1.15× slower). Against pugixml, append is within 1.25× and set-text within 1.3×. The remaining gap (set-attributes) is due to node layout — see TODO 90 for the compact-storage migration plan.
LTO is enabled by default for Release and RelWithDebInfo builds.
Disable with -DTAURUS_ENABLE_LTO=OFF:
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build # LTO is on automatically
# or explicitly:
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DTAURUS_ENABLE_LTO=ONcmake -B build -S . \
-DCMAKE_BUILD_TYPE=Release \
-DTAURUS_BUILD_BENCHMARKS=ON
cmake --build build
# Run individual benchmarks:
./build/benchmarks/bench_dom_taurus
./build/benchmarks/bench_sax_taurus
./build/benchmarks/benchmark_write # vs pugixml + libxml2
./build/benchmarks/bench_xpath_pugixml # XPath vs pugixmlCI uploads a benchmark-results-<os> artifact per push with JSON
Markdown output from every benchmark binary.
A vcpkg port pattern follows the jemalloc convention (see
tamatebako/jemalloc/ports/jemalloc/). The library ships with:
-
A
vcpkg.json(manifest) for vcpkg consumption. -
A
portfile.cmaketemplate for vcpkg port submission. -
A
usagefile documenting the linkage pattern.
# portfile.cmake (excerpt — see repo for full version)
vcpkg_cmake_configure(
SOURCE_PATH "${SOURCE_PATH}"
OPTIONS
-DTAURUS_BUILD_CLI=OFF
-DTAURUS_ENABLE_UTF8PROC=ON
-DTAURUS_ENABLE_ICONV=ON
)
vcpkg_cmake_install()
vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/taurus)-
CMake ≥ 3.20
-
C99 compiler (GCC, Clang, MSVC, MinGW)
-
Optional: utf8proc (Unicode), iconv (encoding conversion), Doxygen (API docs)
No runtime dependencies when built without utf8proc/iconv.
| Workflow | Triggers | What it does |
|---|---|---|
|
Every push/PR |
Build, run all 103 specs across 13 modules. |
|
Every push/PR |
Build with AddressSanitizer; verify zero leaks / errors. |
|
Nightly cron |
libFuzzer for 5 minutes; report any crashes. |
taurus/
src/ # library + CLI + tests source
include/ # public C API headers
taurus/ # internal C source
dom/ # DOM node types + pool
parse/ # parser
xpath/ # XPath evaluator
sax/ # SAX parser
encode/ # UTF-16 + iconv
serialize/ # output writer
memory/ # pool allocator
dtd/ # DTD subsystem
cli/ # command-line tool
test/ # 345 specs across 14 modules
benchmark/ # libxml2 / pugixml comparisons
bindings/ruby/ # Ruby FFI binding (TODO 118)
TODO.fix/ # local scratchpad (gitignored)
archive/ # historical / disabled code
.github/workflows/ # CI
docs/ # README, building guide, FFI design
====
== Roadmap
See link:archive/README.md[archive/README.md] for historical
context, and link:docs/FFI.md[docs/FFI.md] for the FFI roadmap.
Shipped in v0.3.0:
* ✓ Zero-copy borrowed text nodes (TODO 115)
* ✓ Pool-routed Parser struct (TODO 114 Phase 3)
* ✓ Streaming SAX state machine (TODO 116 Phases A-C — recursive parser removed)
* ✓ XInclude ownership transfer + cycle detection (TODO 117 Phases A-C)
* ✓ DTD content-model memoization (TODO 119)
* ✓ XPath bytecode VM, complete + wired in (TODO 120 Phases A-E)
* ✓ Compact-pointer int32 overflow fix for macOS (TODO 121)
* ✓ Ruby FFI binding (TODO 118)
Shipped post-v0.3.0 (XPath perf track):
* ✓ SAX API exported from shared library (TODO 122 — unblocks Ruby SAX)
* ✓ XPath diagnostic benchmark suite (TODO 123)
* ✓ XPath bytecode VM inline dispatch + bytecode cache (TODO 120 Phase F)
* ✓ Lazy namespace init — 5-9× faster per-eval floor (TODO 125)
* ✓ Specialized child/attribute/self/parent axes (TODO 126)
* ✓ Specialized descendant / descendant-or-self axes (TODO 127)
* ✓ Simple predicate fast paths: `[@attr]`, `[@attr='lit']`, `[N]` (TODO 128)
* ✓ Specialized absolute paths `/foo` `//foo` with `//name` fusion (TODO 129)
* ✓ Inline VM opcodes for common functions — `count`, `sum`, `string`, etc. (TODO 130)
* ✓ Iterative descendant walk + result pre-alloc (TODO 131)
* ✓ Per-document element index for O(1) descendant queries (TODO 132)
* ✓ Attribute index infrastructure for predicate fast paths (TODO 133)
* ✓ Fused axis+predicate opcodes — `descendant::*[@id]` now BEATS libxml2 (TODO 134)
* ✓ Fast inline nodeset_add for VM hot paths (TODO 135)
* ✓ Descendant-or-self fused predicate opcodes (TODO 136)
* ✓ Memcpy fast path for index-backed descendant queries — Taurus BEATS libxml2 on ALL XPath benchmarks (TODO 137)
Planned:
* Python bindings via `cffi`.
* Rust bindings via `bindgen`.
* Ruby SAX binding (now unblocked by TODO 122).
* Extend element index to relative-descendant queries (currently
only absolute paths and root-context descendant use it).
* Pre-evaluate function args via VM for the remaining string
functions (`concat`, `contains`, `substring`).
* Doxygen API reference polish.
== Contributing
Issues and pull requests at
https://github.com/lutaml/taurus[github.com/lutaml/taurus].
For C contribution, see link:docs/guide/building.md[docs/guide/building.md].
For the testing policy, see `test/README.md`.
== Acknowledgments
This project draws structural inspiration from several long-running
C projects in the wider ecosystem:
* *jemalloc* (Tebako fork) — memory model and CI patterns.
* *libxml2* — public API ergonomics.
* *pugixml* — pool-based XML DOM, performance targets.
== License
MIT. See link:LICENSE.md[LICENSE.md].