How to cite this paper
Delpratt, O'Neil. “Achieving Near-Native Performance in XSLT, XQuery, and XPath Using Host-Language User
Extension Functions.” Presented at Balisage: The Markup Conference 2026, Washington, DC, August 3 - 7, 2026. In Proceedings of Balisage: The Markup Conference 2026. Balisage Series on Markup Technologies, vol. 31 (2026). https://doi.org/10.4242/BalisageVol31.Delpratt01.
Balisage: The Markup Conference 2026
August 3 - 7, 2026
Balisage Paper: Achieving Near-Native Performance in XSLT, XQuery, and XPath Using Host-Language User
Extension Functions
O'Neil Delpratt
O'Neil Delpratt joined Saxonica from a research project at the University of
Leicester in 2010. He is a co-developer of the Saxon product, with specific
responsibility for Saxon on .NET, and the C/C++, PHP and Python product SaxonC.
Before joining Saxonica, he completed his post-graduate studies at the
University of Leicester. His thesis title was "In-memory Representations of XML
documents" which coincided with a C++ software development of a memory efficient
DOM implementation, called Succinct DOM. O'Neil regularly publishes and presents
papers at various XML conferences, and was an invited expert on the W3C XQuery
Working Group.
Copyright © 2026 by the author. Used with permission.
Abstract
XSLT, XQuery, and XPath provide powerful declarative languages for processing XML
and related data formats XSLT30 XQUERY31.
However, some workloads involve computationally intensive
operations or integration requirements that are more naturally expressed in a host
programming language.
Modern XML processors allow user-defined extension functions to be implemented in
host languages (e.g., in Java, C#, JS, C++, Python and PHP) and invoked directly from
XSLT, XQuery, and XPath expressions SAXON
BASEX.
This paper investigates the performance and practical benefits of using
host-language user extension functions where necessary as an alternative to
implementing equivalent logic directly in XSLT, XQuery, or XPath. As a case study,
we implement and benchmark equivalent computational workloads using extension
functions written in Java, C++ and Python. We examine the trade-offs between
declarative purity, execution performance, and integration flexibility, including
the overhead of crossing language boundaries and the circumstances under which
host-language implementations can outperform equivalent XML-language
expressions.
The results demonstrate that carefully designed extension functions can achieve
performance approaching that of the host language itself while remaining fully
integrated within XML processing. These findings suggest that extension functions
provide a valuable mechanism for addressing performance-critical and
integration-oriented requirements without abandoning the declarative programming
model offered by XSLT, XQuery, and XPath.
Table of Contents
- Introduction and Motivation
- User Extension Function Implementations
-
- Why extension functions?
- Implementation in SaxonJ (Java)
- Implementation in SaxonC (C++/Python/PHP)
- Experimental anaylsis
-
- Setup
- Running Times
- Discussion
- Conclusion
- Appendix A. Overview of Saxon Products
-
- SaxonJ
- SaxonC
- SaxonJS
Introduction and Motivation
The motivation for this work stems in part from Cython, a superset of Python that
enables developers to achieve performance closer to that of C while retaining much
of
the Python programming model CYTHON.
The performance gains of Cython are achieved through several mechanisms. Firstly,
Cython allows variables, function parameters and return values to be declared using
static C types, reducing the overhead associated with Python's dynamic type system.
Secondly, statically typed values can be compiled directly in C, avoiding runtime
type
checks and object allocations. Thirdly, Cython can invoke C functions and libraries
directly, bypassing the Python interpreter for computationally intensive tasks. The
generated code remains interoperable with the Python runtime and object model.
The final point mentioned above is particularly relevant to this work because it
parallels the way SaxonC integrates Python extension functions with XSLT, XQuery,
and
XPath (Details on the Saxon variants, see Appendix A). In both cases, computationally intensive
operations can be delegated to Python code while remaining accessible from a
higher-level programming environment.
There are other tools that mix host language functionity too. For example, the XForms
implementation Saxon-Forms SAXONFORMS was built using SaxonJS to
run in web browsers. The extensibility of Saxon-Forms is important for certain
applications, where some XForms functions not in the XPath specification are implemented
as user-defined functions written directly in JavaScript. The paper also describes
the
use of application logic implemented in JavaScript to meet the needs of its users.
These
functions can then be imported and invoked from XPath expressions used within XForms
applications.
This raises an interesting question for XML technologies: can host-language extension
functions provide similar benefits for XSLT, XQuery, and XPath applications, allowing
developers to preserve a declarative XML-processing architecture while selectively
benefiting from the performance characteristics of native languages?
The experimental analysis presented in this paper focuses on SaxonJ and SaxonC (specifically
the commercial products - named as EE).
SaxonJ is implemented entirely in Java and is designed to run on a traditional JVM.
SaxonC, by
contrast, is built using GraalVM Native Image technology (AOT compiler) GRAALVM, a
JVM implementation capable of compiling
Java applications ahead of time into native code and packaging them as shared
libraries.
Effectively, a lightweight VM runtime executes within the native library, allowing
native applications to invoke functionality implemented in Java. This enables applications
written in other languages, such as in C and C++,
to call Saxon without requiring a separate Java installation.
The resulting binary includes the compiled SaxonJ code together with the required
JDK
libraries and runtime components needed to execute the native image. Creating a native
image offers several advantages, including a C/C++ API, faster startup times, lower
resource usage, greater flexibility when integrating with other programming languages,
and simplified deployment and packaging. In addition to its C and C++ APIs, SaxonC
provides language bindings for Python and PHP.
Before presenting our experimental analysis and results, we first describe how
extension functions are implemented in Saxon. The discussion focuses on SaxonJ for
Java
and SaxonC, which adopts a different approach to integrating and supporting host
languages such as C++, Python, and PHP. We then detail our experimental design,
performance analysis and discussion of results then finally conclusion on findings.
User Extension Function Implementations
Extension functions allow code written in a host programming language (for example:
Java, C++,
Python or PHP) to be invoked directly from a function call in an XPath expression
(whether in XSLT, XQuery, or pure XPath). For this to work the Saxon processor needs
to
be able to locate the executable code corresponding to a particular function name.
The
function arguments need also to be available and are passed as either XDM types or
converted to the host language type
system, with the opposite conversion applied to the return value of the function.
Why extension functions?
Before describing the various user extension function implementations, it is worth considering why extension functions are useful and why a
developer might choose to use them. One reason is that modern XML processors, such
as Saxon or BaseX,
provide mechanisms for extending XSLT, XQuery, and XPath
with functions written in a host programming language. This flexibility allows XML
technologies to be integrated more easily into larger software systems and enables
developers to reuse existing libraries and functionality written in languages such
as Java, C++, Python, and PHP.
Another motivation is performance. Certain operations are more naturally expressed
and executed in imperative programming languages than in XSLT or XPath. For example,
computationally intensive algorithms, numerical calculations, bitwise operations,
or
calls to specialised native libraries may incur significant overhead when
implemented purely in XSLT or XPath. In such cases, implementing the computationally
intensive portion of the workload as an extension function can provide substantial
performance benefits while still allowing the overall application logic to be
expressed in XSLT, XQuery, or XPath.
Implementation in SaxonJ (Java)
In SaxonJ at a high level, the implementation works as follows
SAXONJ-EX: The user creates a Java class that extends
ExtensionFunctionDefinition class:
-
Class declares:
-
The user then has to register the function with the Saxon processor
configuration.
-
When a call on the extension function is encountered during XPath,
XSLT, or XQuery execution, Saxon invokes the corresponding
ExtensionFunctionCall object.
-
Arguments to the call function are passed as an array of objects of
the XDM data model, and the return type must also be an XDM value.
Implementation in SaxonC (C++/Python/PHP)
The implementation of user-defined extension functions in SaxonC builds upon the
SaxonJ implementation. It extends the ExtensionFunctionDefinition class; however,
to
support functions written across language boundaries for C++, Python, and PHP, the
underlying implementation differs significantly
SAXONC-EX.
The user function registration is done through the SaxonProcessor using a JSON
description that specifies the function namespace, name, function arity, argument
types, and return type.
The host-language function is supplied to Saxon as a function pointer. When an
XPath, XSLT, or XQuery executable invokes the extension function, GraalVM is used
to
call the user-supplied function through a thin C layer, which dispatches the
callback to the corresponding C++, Python, or PHP implementation. During this
process, the function arguments are converted to the appropriate host-language
primitive type system, if possible. For the XDM tree node types these cannot be converted
to a primitive
type therefore we leave these as wrapper objects in C++, Python or PHP. The return
value is
converted back into an XDM type so that it can be consumed
by the Saxon processing engine.
See example below which shows in C++ the setup of a user function in XSLT. Its a
similar approach in Python and PHP. But in Python the user has the additional option
to request conversion of XDM types to primitive python types where possible. Also
in
Python the method argument has a keyword argument to get access to the
PySaxonProcessor object used for the configuration.
This method illustrates a CPU intensive task for calculating the square root of a
set of numbers iterated a certain number of times. The arguments match those defined
in the signature (see below) and are passed in the XSLT/XQuery/XPath call:
XdmValue * calculate(int argc, XdmValue ** argv) {
long iterations =
((XdmAtomicValue *)argv[0]->getHead())->getLongValue();
double otherData =
((XdmAtomicValue *)argv[1]->getHead())->getDoubleValue();
double result = 0.0;
for (long i = 1; i <= iterations; i++) {
result += std::sqrt(static_cast<double>(i)) + otherData;
}
return processor->makeDoubleValue(result);
}
The registration of the calculate user function is given below.
Notice the pointer to the calculate function is passed to the SaxonProcessor which
is used in the round-tripping to call the function when required in the XSLT, XPath
or in XQuery:
proc->registerUserFunction("www.host-lang-func.com", &calculate,
"{ \"calculate\": {"
" \"as\": \"xs:double\","
" \"param\": ["
" \"xs:integer\", "
" \"xs:integer\""
" ],"
" \"arity\": [2]"
" }"
"}");
The XSLT code that calls the C++ calculate function is shown below. The namespace used to register the function must match the namespace
declared in the XSLT; otherwise, the function will not be found. For example:
xmlns:func="www.host-lang-func.com"
The
namespace URI specified during function registration must be identical to the URI
used in the XSLT declaration.
<xsl:template name="funcCall">
<results>
<!-- Repeat the expensive function 100 times -->
<xsl:for-each select="1 to $rpt">
<run number="{.}">
<xsl:value-of select="func:calculate($input, .)"/>
</run>
</xsl:for-each>
</results>
</xsl:template>
Experimental anaylsis
In this section, we present a number of examples. For each of them we compare the
execution time performance of a pure XSLT stylesheet that uses the
xsl:function instruction to perform a task with an equivalent modified
XSLT stylesheet that perform the same task using a user-defined extension function.
To
broaden the scope of the experiments, we execute the stylesheets in Java, C++, and
Python implementations. This is achieved through the SaxonJ s9api and SaxonC APIs,
with
equivalent user extension functions implemented in each host language.
The experiments are designed to address several questions. Is pure XSLT faster in
all
cases? Can user extension functions be used to improve performance for certain classes
of tasks? What overheads are introduced when invoking host-language functions from
XSLT,
and under what circumstances should such functions be avoided? By examining both
computational and XML-processing workloads, we aim to identify the situations in which
extension functions provide the greatest benefit and the trade-offs involved in their
use.
Setup
The experiments were conducted using the release version 13.1 for both SaxonJ and
SaxonC. At the time of writing, this version was a pre-release. The test machine was
an Apple MacBook Pro equipped with an M3 Max processor and 64 GB of memory.
The performance analysis is based on four benchmark scenarios, each implemented
both as a pure XSLT xsl:function and as an equivalent user-defined
extension function written in Java, C++, and Python:
-
math:sqrt(number) — performs square-root calculations over
10 million numbers, repeated 100 times. This benchmark is intended to
represent a CPU-intensive computational workload.
-
XSLT function:
<xsl:function name="xf:calculate" as="xs:double">
<xsl:param name="iterations" as="xs:integer"/>
<xsl:param name="context" as="xs:integer"/>
<xsl:sequence select="
sum(
for $i in 1 to $iterations
return math:sqrt($i) + $context
)
"/>
</xsl:function>>
-
The C++ function:
XdmValue * calculate(int argc, XdmValue ** argv) {
long iterations =
((XdmAtomicValue *)argv[0]->getHead())->getLongValue();
double context =
((XdmAtomicValue *)argv[1]->getHead())->getDoubleValue();
double result = 0.0;
for (long i = 1; i <= iterations; i++) {
result += std::sqrt(static_cast<double>(i)) + context;
}
return processor->makeDoubleValue(result);
}
The Java code is similar:
public XdmValue call(XdmValue[] arguments) throws SaxonApiException {
long iterations = ((XdmAtomicValue) arguments[0].itemAt(0))
.getLongValue();
long context = ((XdmAtomicValue) arguments[1].itemAt(0))
.getLongValue();
double total = 0.0;
for (int i = 1; i <= iterations; i++) {
total += Math.sqrt(i) + context;
}
return new XdmAtomicValue(total);
}
The equivalent Python code is given below. For the Python
extension we do the following:
def calculate(iterations, context, **kwargs):
result = sum((math.sqrt(i) + context)
for i in range(1, iterations + 1))
return result
-
abs(number) — performs a simple absolute-value calculation
repeated 100,000 times. This benchmark is intended
to measure the overhead associated with calling an extension function
for a relatively inexpensive operation.
-
XSLT function:
<xsl:function name="f:calculate" as="xs:double">
<xsl:param name="iterations" as="xs:integer"/>
<xsl:sequence select="abs($value)"/>
</xsl:function>
-
Java function: new
XdmAtomicValue(Math.abs((double)value)).
Similarly in C++ this would be
processor->makeDoubleValue(std::abs((double)value))
-
Tree navigation using a predicate on a 10 MB XMark XML document. The
$person variable is given as a string parameter to the
stylesheet.
-
XSLT function:
$node//person[name = $person]
-
In Java, C++ and Python we would need to traverse the tree
using the XDM node API and look for the person with "name"
element that matches a certain string name.
-
Tree navigation and string-length computation over all nodes in a 100
KB XMark XML document.
-
XSLT function:
<xsl:function name="xf:process-node" as="xs:integer">
<xsl:param name="node" as="node()"/>
<xsl:param name="iterations" as="xs:integer"/>
<xsl:sequence select="
sum(
for $i in 1 to $iterations
return
sum(
for $n in $node/descendant-or-self::node()
return string-length(string($n))+$i
)
)
"/>
</xsl:function>
-
In Java, C++ and Python we can use a recursive function to
get the string-lengths on the XDM node in the tree.
Running Times
The following table shows the execution times, measured in seconds.
Table I
|
SaxonJ - XSLT (Java) |
SaxonJ-XSLT (Java+Func) |
SaxonC-XSLT (Python) |
SaxonC-XSLT (Python+Func) |
SaxonC-XSLT (C++) |
SaxonC-XSLT (C++ +Func) |
| math:sqrt() and sum calculation |
9s |
0.63s |
71s |
55s |
71s |
0.76s |
| abs() |
0.052s |
0.066s |
0.08s |
0.23s |
0.086s |
0.21s |
| Tree-walk and string length calc |
7.2s |
7.6s |
28.1s |
77.8s |
23s |
202s |
| Tree-walk with predicate |
0.022s |
0.042s |
0.017s |
0.342s |
2.59s |
2.64s |
Discussion
It is worth mentioning again that SaxonC is built from SaxonJ using GraalVM
native-image technology to produce native code. Consequently, we expect some
overhead when invoking callbacks in SaxonC, as the C++ and Python APIs must
ultimately interact with the underlying Saxon implementation. However, GraalVM is
highly optimised and can, in some cases, deliver faster execution times due to
reduced startup overhead, the absence of JVM warm-up costs, and the ability to
execute operations at near-native speed.
Comparison of the execution times across the different benchmark scenarios reveals
several interesting findings, which we summarise as follows:
-
When is it advisable to use user extension functions?
The math:sqrt() benchmark test is
a clear example where the use of user extension functions across all
host languages delivered significantly better performance than running
pure XSLT in the same environment. In the case of Java and C++, the
extension-function implementations achieved performance close to optimal
native execution speed. As a baseline running the math:sqrt()
in pure Java code that is without any XSLT, ran in 0.673
seconds. The pure XSLT implementation in
Java completed in approximately 9 seconds, whereas the version using an
extension function completed in just over 1 second, making it
approximately seven times faster.
We observed similar execution times for the C++ extension function,
which also completed in just over 1 second. In this benchmark, most of
the work was performed within the C++ function itself. By contrast, the
pure XSLT implementation executed through SaxonC C++ API ran in
approximately 71 seconds to completion. This indicates that, for this
CPU-intensive workload, pure XSLT execution in SaxonC was almost eight
times slower than pure XSLT execution in Java. In general, we observed
here the overhead associated with crossing the boundary between the
SaxonC APIs and the underlying implementation was approximately four
times greater than in Java, although this varied depending on the nature
and duration of the workload.
The SaxonC Python implementation showed similar times for the pure
XSLT execution to those observed in C++. The Python extension function
was completed in approximately 55 seconds. This is still faster than
pure XSLT executed in Python. But this raises the question of why the
square-root benchmark performs substantially faster in C++ than in
Python. The difference can largely be explained by the overhead of
repeated execution within the Python interpreter, including dynamic type
handling, function invocation, and object management. By contrast, the
C++ implementation is compiled directly to machine code, allowing the
computation to execute much more efficiently.
-
Tree navigation is a clear winner for pure XSLT.
Across all languages, the tree-navigation benchmark using a predicate
showed that pure XSLT significantly outperformed the equivalent
extension-function implementation. In Saxon-EE, a query such as
//x[a=42] is optimized by building a document-level
index, meaning that if such a query is executed multiple times, all but
the first execution operate in constant time, independent of document
size. The user-written extension function does not attempt any similar
optimization. On Java and Python, pure XSLT performed better than user
extension functions: 2.5 times faster for Java and 15 times faster for
Python. The results for C++ were much closer between pure XSLT and C++
user function, which may be due to inefficiencies in the allocation and
deallocation of objects created during tree navigation. Each callback
also requires crossing the langauge boundary between the host-language
API and the underlying Saxon implementation in Java.
Another explanation is for Java and Python, the objects lifetime are
largely managed by the garbage collection, the additional
memory-management overhead in C++ may contribute to the observed
slowdown. The tree-walk benchmark involving string retrieval also showed
that pure XSLT execution in SaxonC was approximately four times slower
than the corresponding Java execution. Unlike the pure Java
implementation, SaxonC incurs additional overhead because string values
must be copied when crossing the boundary between the underlying Java
implementation and the C++ API layer.
-
For simple operations pure XSLT versus extension functions
shows little advantage. The abs() benchmark
showed only a small difference between the pure XSLT implementation and
the extension-function implementation in Java. For such a simple
operation, the overhead of invoking an extension function outweighs most
of the potential benefits. The small difference can be concluded as
machine differences at the time of execution. In these cases, pure XSLT
is generally preferable because it avoids the additional complexity
associated with implementing and maintaining user-defined extension
functions.
Conclusion
In summary, the results show that combining XSLT or XQuery with host-language
user-defined extension functions can significantly improve performance. However, their
use should be guided by the nature of the task being performed.
Extension functions should be viewed as a powerful tool for addressing computationally
intensive operations or tasks that are difficult to express efficiently in XSLT or
XQuery alone. In some cases, the performance gains may arise from limitations or
inefficiencies in the implementation of a particular XSLT or XQuery solution. Where
there is a substantial performance gap, as demonstrated by the math square root
benchmark and other computationally intensive or recursive operations, the use of
a
host-language extension function can provide dramatic improvements in execution
time.
By contrast, for XML-centric operations such as tree navigation and predicate
evaluation, pure XSLT often remains the most efficient solution. This is particularly
true when the XPath optimizer finds an smart execution strategy, for example, construction
of an index as in this example.Developers should
therefore consider extension functions selectively, using them where they provide
a
clear benefit while continuing to rely on the strengths of the XSLT and XQuery
processing model for document-oriented tasks.
Appendix A. Overview of Saxon Products
This appendix provides a brief overview of the Saxon product family to give context
for the experiments presented in this paper. Saxon is an implementation of the W3C
Recommendations for XSLT, XQuery, XPath, and XML Schema validation, and forms part
of a
wider ecosystem of XML technologies used for transformation, querying, validation,
and
document processing SAXON.
Saxon is available in several product variants, each designed to support different
deployment environments and application requirements. The user-defined extension
function feature described in this paper is available in the commercial edition of
Saxon, called Saxon-EE.
SaxonJ
SaxonJ is the Java implementation of Saxon and is the reference implementation
from which the other Saxon products are derived. It provides APIs through the s9api
interface and supports execution of XSLT, XQuery, XPath, and XML Schema validation
on the Java Virtual Machine (JVM). SaxonJ also provides the extension function
framework used to implement user-defined functions in Java.
SaxonC
SaxonC is built from SaxonJ using GraalVM Native Image technology to produce
native executables and libraries. It provides native APIs for C and C++, together
with language bindings for Python and PHP. Beginning with SaxonC 13, applications
can implement user-defined extension functions directly in C++, Python, and PHP,
allowing XSLT, XQuery, and XPath expressions to invoke host-language code through
the SaxonC APIs.
SaxonJS
SaxonJS is the JavaScript implementation of Saxon designed for execution within
web browsers and JavaScript environments. It supports client-side execution of XSLT
3.0 stylesheets and forms the execution engine for applications such as Saxon-Forms.
References
[XSLT30] Kay, Michael. XSL Transformations
(XSLT) Version 3.0, 8 June 2017. W3C Recommendation. http://www.w3.org/TR/xslt30
[XQUERY31] Robie, Jonathan, Dyck, Michael, and Spiegel, Josh.
XQuery 3.1: An XML Query Language, 21 March 2017. W3C Recommendation. https://www.w3.org/TR/xquery-31
[CYTHON] Cython.
https://cython.readthedocs.io/
[SAXONFORMS] Delpratt, O'Neil and Lockett, Debbie.
Implementing XForms using interactive XSLT 3.0.
Presented at XML
Prague 2018. In Proceedings of XML Prague 2018: http://archive.xmlprague.cz/2018/files/xmlprague-2018-proceedings.pdf
[SAXON] SaxonJ and SaxonC Products.
Saxonica. https://www.saxonica.com/
[SAXONJ-EX] SaxonJ - Java extension functions:
simple interface. Saxonica. https://www.saxonica.com/documentation13/index.html#!extensibility/extension-functions-J/ext-simple-J
[SAXONC-EX] SaxonC - C++ and Python Extension
functions. Saxonica. https://www.saxonica.com/saxon-c/documentation13/index.html#!extensibility
[BASEX] BaseX Java Bindings.
BaseX. https://docs.basex.org/main/Java_Bindings
[GRAALVM] GRaalVM.
https://www.Graalvm.org/