-
XML 1.0 parsing with UTF-8 validation and optional encoding conversion (UTF-16, ISO-8859, Shift-JIS, EBCDIC, and others via iconv).
-
XPath 1.0 engine implementing all 13 axes, 27 functions, and 15 operators.
-
SAX API for event-driven streaming on documents that don’t fit in a DOM.
-
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. -
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. -
Zero required runtime dependencies — utf8proc and iconv are optional features, not prerequisites.
-
Stable C ABI with a documented FFI contract; bindings planned for Ruby, Python, and Rust. 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 planned:
-
Ruby — via
ruby-ffi(no compilation) -
Python — via
cffi(header-aware) -
Rust — via
bindgen+ idiomatic wrapper
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 |
| 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/ # 103 specs across 13 modules
benchmark/ # libxml2 / pugixml comparisons
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.
In-flight:
* Ruby / Python / Rust bindings.
* Doxygen API reference polish.
* Split of `taurus.c` (2900 lines → focused modules).
* Unified string ownership model.
== 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].