LLVM/clang Plugins¶
libxo ships two optional LLVM/clang plugins that operate on
xo_emit()-family calls at compile time:
xo_validate — a clang AST plugin that checks
libxoformat strings for syntax errors and checks each format string’s field descriptors against the types and count of the arguments actually passed, much like-Wformatdoes forprintf().xo_precompile — an LLVM IR pass that finds
libxocalls whose format string is a compile-time constant, parses that format string during the build, and rewrites the call toxo_emit_cached(), passing a pre-parsed field table as a constant. This removes the runtime cost of parsing the format string on every call.
Both plugins share a single format-string parser (the same one libxo uses internally, reached through a small C shim so the C++ plugin code never has to include libxo’s non-C++-safe internal headers), so their understanding of field syntax never drifts from the library’s own.
These plugins are entirely optional. Code built without them runs
exactly as it always has, parsing format strings at run time. Neither
plugin changes libxo’s public behavior or output — xo_precompile
specifically only ever changes when a format string is parsed, never
what it means.
libxo includes xocc, a compiler wrapper that already knows
these paths (and the link-time -lxo flags); see The “xocc” compiler wrapper for
using it instead of setting these flags by hand.
Building the plugins¶
The plugins require LLVM/clang development files (specifically
llvm-config and the matching Clang cmake package) and cmake. They
are built as a cmake subproject, driven from the normal
autoconf/automake build via --with-llvm-config:
../configure --with-llvm-config=/opt/local/libexec/llvm-19/bin/llvm-config \
--prefix ~/work/root
If --with-llvm-config is omitted (or llvm-config can’t be found),
configure disables the plugins entirely and the rest of the build is
unaffected.
On macOS, the plugins are built with the same clang that provides
llvm-config, which is not Apple’s system clang and does not know
where the macOS SDK headers live. configure locates the SDK
automatically via xcrun; override it with --with-sdk-path=PATH, or
suppress the -isysroot flag entirely with --with-sdk-path=no:
../configure --with-llvm-config=/opt/local/libexec/llvm-19/bin/llvm-config \
--with-sdk-path=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
After configuring, build normally; the plugins build alongside the rest
of the tree and install into ${prefix}/lib:
cd build
make CFLAGS='-O2'
make install
Building just the plugins (skipping the rest of libxo) can be done from their subdirectory:
cd build/llvm-plugins
make
This produces validate/xo_validate.so and precompile/xo_precompile.so.
Both are cmake-driven subprojects; configure seeds each one’s cmake
cache with the right LLVM_DIR, Clang_DIR, target architecture, and
install prefix, so plain make in that directory is sufficient — there
is no separate cmake configure step to run by hand.
xo_validate: format-string checking¶
xo_validate is a standard clang -fplugin AST plugin. It runs
alongside the compiler’s normal parsing and codegen (it does not
replace or suppress them), inspecting every call to xo_emit() and its
variants (xo_emit_h, xo_emit_hf, xo_emit_hvf, the _p wrappers,
etc.) whose format string is a compile-time string constant. For each
one it checks:
Syntax — malformed field descriptors, such as an unclosed
{.Argument count — too few or too many arguments for the fields in the format string.
Argument type — the type each field’s display format expects (following the same length-modifier rules as
printf, e.g.%ldexpectslong,%zuexpectssize_t) is compared against the actual argument’s type, after the usual varargs promotions.Style — a number of
xolint-style checks on field naming and anchor usage. Leading digits, stray%characters, and anchor width/format mismatches are always checked; underscores (instead of hyphens), upper-case letters, and names shorter than the minimum length are checked only when lint mode is enabled (see below).
Diagnostics are reported through clang’s normal diagnostic engine, so they appear exactly like any other compiler diagnostic, with source location and a caret pointing at the offending call:
xo_validate_demo.c:22:13: error: libxo: format expects 2 argument(s) but 1 provided
22 | xo_emit("{:name/%s} {:age/%d}\n", "bob");
| ^
Tuning diagnostics¶
Two -mllvm flags control how xo_validate reports problems:
`-mllvm -xo-validate-errors-as-warnings` — by default, syntax, argument count, and argument type problems (checks 1-3 above) are reported as errors, so a build with the plugin loaded fails on them. This is deliberate: these are almost always real bugs, and a checker that only ever warns is too easy to ignore. This flag downgrades them to warnings instead, which is useful when first introducing the plugin to a codebase with an existing backlog of violations, or to keep a build green while an individual false positive gets sorted out upstream. Style diagnostics (check 4) are always warnings and are unaffected by this flag.
`-mllvm -xo-validate-lint` — enables the additional cosmetic naming checks described under Style above (underscores, upper case, short names). These are off by default since they’re closer to style preference than correctness; the always-on style checks (leading digits, anchor mismatches) still run either way.
Both flags can be combined with any of the -fplugin= invocations
shown below, e.g.:
clang -c \
-fplugin=/path/to/xo_validate.so \
-mllvm -xo-validate-errors-as-warnings \
-mllvm -xo-validate-lint \
-I/path/to/xo/include \
myprogram.c -o myprogram.o
Invoking xo_validate¶
Invoke the validate plugin using -fplugin=, pointing at the built
.so:
clang \
-fplugin=/path/to/xo_validate.so \
-I/path/to/xo/include \
-isysroot $(xcrun --show-sdk-path) \
myprogram.c
It can also be loaded during a normal compile (the diagnostics appear alongside the compiler’s own, and compilation proceeds as usual):
clang -c \
-fplugin=/path/to/xo_validate.so \
-I/path/to/xo/include \
myprogram.c -o myprogram.o
To integrate it into a real build, add the two -fplugin=... /
-I... flags to CFLAGS for the translation units you want checked;
there is nothing else to configure or link against.
Since it only inspects the AST and does not need to link, using the
-fsyntax-only option is the fastest way to run it standalone, if
only syntax checking is needed.
xo_precompile: build-time format parsing¶
xo_precompile is an LLVM new pass-manager module plugin, loaded
using -fpass-plugin=. It runs early in the optimization pipeline
(PipelineStart, before inlining or constant-folding can obscure a
call or the string it references), and looks for calls to xo_emit()
and its variants where the format-string argument resolves to a
constant C string.
For each such call it will:
parse the format string with libxo’s own parser (via the shared C shim), producing the same offset-based field table (
xo_field_info_t[]) and pre-parsed display-format table (xo_fspec_t[]) thatxo_emit()would otherwise build at run time, on every call.emit those tables as
private constantLLVM globals in the module.rewrite the call to the matching
xo_emit_cached*()entry point (xo_emit→xo_emit_cached,xo_emit_hf→xo_emit_cached_hf, and so on), passing a pointer to the generated table ahead of the format string and value arguments.
At run time, xo_emit_cached() skips straight to formatting using the
supplied table instead of first scanning the format string to build
one. Output is byte-for-byte identical to what xo_emit() would have
produced from the same format string and arguments — the pass changes
only when the format string gets parsed, never the result.
If precompiling it isn’t possible or safe, calls are left untouched,
falling back to ordinary xo_emit() and parsing format strings at run
time. This is needed when:
the format string isn’t a compile-time constant (e.g. it was built at run time, or came from a variable),
the shim reports a parse error for it (the ordinary runtime path will report the same error, so nothing is silently swallowed), or
the callee isn’t one of the known
xo_emit*()names, or is called indirectly through a function pointer.
Because the rewrite happens at the IR level, it works regardless of
what source-level macros or wrappers were used to reach xo_emit(), as
long as the call and its format-string argument survive to IR in a form
the pass can see. It does not require any source changes — the same
.c file compiles correctly whether or not the pass is loaded, just
faster when it is.
Invoking xo_precompile¶
Invoke the precompile module by passing the -fpass-plugin= option on
a normal compile; nothing else about the command line changes, and the
resulting object file links against the same libxo as an unmodified
build (xo_emit_cached() and friends are ordinary exported libxo
symbols, not something the plugin invents):
clang -c \
-fpass-plugin=/path/to/xo_precompile.so \
-I/path/to/xo/include \
myprogram.c -o myprogram.o
clang myprogram.o -L/path/to/xo/lib -lxo -o myprogram
To see the effect directly, dump LLVM IR instead of an object file and
look for the generated .xo_fields.* / .xo_fspecs.* / .xo_fcache.*
globals and the xo_emit_cached call:
clang -S -emit-llvm \
-fpass-plugin=/path/to/xo_precompile.so \
-I/path/to/xo/include \
myprogram.c -o - | grep -A2 xo_fcache
xo_validate and xo_precompile are independent but can be used
together — one checks correctness at the AST level, the other rewrites
at the IR level — so both flags are normally passed on the same
command line:
clang -c \
-fplugin=/path/to/xo_validate.so \
-fpass-plugin=/path/to/xo_precompile.so \
-I/path/to/xo/include \
myprogram.c -o myprogram.o
Wiring into a build¶
For a project already building against libxo, add the plugin flags to
CFLAGS:
make CFLAGS='-O2 -fplugin=/path/to/xo_validate.so -fpass-plugin=/path/to/xo_precompile.so'
Caveats and known limitations¶
Only calls with a literal, compile-time-constant format string can be precompiled. A format string built with
sprintf(), concatenation of a runtime value, gettext lookups that select between strings at runtime, etc. is left as an ordinaryxo_emit()call.xo_precompilecurrently registers atPipelineStart. At-O0this still fires under the current New Pass Manager, but if a future LLVM version changes whenPipelineStartcallbacks run at-O0, building the precompiled translation units at-O1or higher is the fallback.Both plugins are LLVM/clang-specific; there is no equivalent for GCC. Code is fully portable either way — without the plugins it just behaves like ordinary, unmodified libxo.
The plugins are versioned against
xo_field_info_t’s in-memory layout viaXO_EMIT_CACHE_VERSION(inxo.h) and a set of_Static_assertlayout checks in the shared C shim (llvm-plugins/validate/xo_parse_shim.c). A libxo built with a mismatched header will refuse the precompiled table at run time and fall back to normal parsing rather than misinterpreting it.