Runtime Libraries
What pyvorin-native 1.0.9's runtime libraries do: typed containers, the Python–native ABI, CPython interop, and where the runtime itself limits speed.
Published Jan 30, 2026
Compiled Pyvorin code is not self-contained. The generated machine code handles arithmetic and control flow itself, but lists, dicts, strings and the object model underneath them live in a set of C runtime libraries that ship inside the wheel and are linked into every compiled shared object. This page describes what those libraries contain, the ABI that governs calls across the Python–native boundary, and — because honesty is the policy — the places where the runtime itself, rather than CPython, is the thing slowing your program down.
It consolidates what was originally planned as two articles: the runtime overview and a separate internals piece on the ABI and typed containers. One boundary deserves one page.
What ships
The runtime is a collection of small, specialised shared objects at the top level of the pyvorin package, plus a second tier of domain kernels under pyvorin/runtime/. The compiler resolves symbols against them at link time; the full set is visible in the compiler's library path table, and all of them were present in the installed 1.0.9 build on this host.
| Library | Responsibility |
|---|---|
nexus_runtime.so | Core object model: the tagged NexusObject struct, type enum, reference counts, built-in operations |
nexus_builtins.so | Built-in function surface linked into compiled modules |
nexus_list_runtime.so | List operations behind 64-bit handles, with per-slot type tags for mixed-type containers |
typed_list_runtime.so | Flat arrays for homogeneous int64 and float64 lists — no per-element tagging |
dict_runtime.so | Int-keyed dictionaries: open-addressed hash table operations |
set_runtime.so, string_runtime.so, bytes_runtime.so | Set, string and bytes operations |
int_runtime.so, float_runtime.so | Big-integer overflow paths and float helpers that preserve Python numeric semantics |
nexus_exceptions.so | Native-side exception raising and propagation |
csv_runtime.so, json_runtime.so, regex_runtime.so, path_runtime.so, file_runtime.so, web_runtime.so | Stdlib-adjacent operations with fast native implementations |
runtime/numeric_kernels*.so, runtime/sort.so, runtime/array2d.so and family | Domain kernels: reductions, matrix helpers, sorting, finance and statistics routines |
Two implementations of the same abstraction coexist here, and the split is the single most important design fact about the runtime. typed_list_runtime stores homogeneous numbers in flat C arrays — a list of floats is literally a contiguous run of double, which is what makes the vectorised loops in Native code generation possible. nexus_list_runtime stores mixed-type containers behind integer handles, each slot carrying a small type tag (integer, string, list, dict, float, bool, null, or an opaque CPython-object handle). Same Python type on the outside; two very different cost structures underneath.
The object model
The core NexusObject is a tagged struct: a type enum, a 64-bit reference count, and a union carrying the value — an integer, a double, a length-and-capacity string buffer, or a pointer to an array of further objects. Dicts chain entries through hash buckets keyed by cached 64-bit hashes. None of this is exotic; it is a compact, C-native re-implementation of the Python data model, designed for one consumer: code the compiler just emitted. The thread-safety story is conventional mutex protection where structures are shared.
Reference counting deserves one observation. Python semantics require that objects die when the last reference goes away, and the native runtime honours that with its own count fields rather than deferring to CPython's. That choice keeps containers independent of the interpreter's allocation internals, at the price of paying the counting cost in native code too. More on that price below.
Alongside the core libraries sit the exceptions runtime, which gives generated code a way to raise and propagate Python exceptions without leaving native execution, and a parallel runtime used when the compiler splits reduction loops across threads. Neither changes the data model; both extend the same handle-and-tag conventions, which is what keeps every compiled module speaking one dialect at the boundary.
Link order is a correctness feature
One loading detail from the compiler source repays attention. When the linker command is assembled, dict_runtime.so is placed before nexus_list_runtime.so, and the newer numeric kernel libraries before the older ones. The comment in the source explains why, and it is worth quoting the reasoning rather than the result: on glibc, with global symbol loading, the first definition of a symbol wins. nexus_list_runtime carries weak dict stubs; load it first and its weak definitions shadow the strong implementations in the dict runtime, and every dict operation in the compiled module silently runs the stub. Ordering the link list is load-bearing. Weak symbols are a legitimate C technique and a recurring source of exactly this bug; baking the order into the compiler rather than documenting it in a README is the engineering judgement call that keeps it fixed.
The Python–native ABI
Every compiled function agrees with its Python wrapper on a contract, defined in the native/abi.py module. Its parts:
Versioning and platform description. The ABI carries an integer version (1 in 1.0.9), and an immutable ABIContract descriptor recording pointer width, size_t width, byte order and a stable platform key. The platform key feeds the compile cache, so a cached object built under one ABI is never loaded under another.
Error codes. Native kernels return small integer status codes — 0 for success, then dtype mismatch, non-contiguous layout, null pointer, bounds violation, overflow and a generic failure. The Python side maps these codes to named exceptions; an overflow in particular is not an error to be papered over but a signal to defer to CPython, where big-integer semantics live.
Calling convention. Scalar values cross the boundary as 64-bit integers or doubles. Containers cross as handles. Array buffers cross as raw pointers plus an explicit length — never as implied by a header the callee must trust.
Alignment. The FFI layer allocates array buffers 64-byte aligned, because non-temporal (streaming) vector stores require alignment to the vector width — 32 bytes for AVX2, 64 for AVX-512. The allocator over-allocates and slices to the aligned offset. A subtle requirement, handled in one place.
The ctypes layer is explicit about signatures: argument types and the return type are set from the compiled function's LLVM type information, with special cases preserved for values that look like integers at the LLVM level but are really string pointers (text kernels borrow UTF-8 straight out of a CPython object) or void* handles. Getting this wrong type-confuses live data; the code comments describe past header/layout divergences caught precisely because extension consumers compile against the same mirror header the runtime guarantees.
Interop with CPython objects
Not everything can be native. When the compiler meets a value it cannot type — an arbitrary object parameter, a heterogeneous container, an unhandled attribute — the boundary machinery boxes it. The mechanism is direct and a little bold: the wrapper passes the CPython object's raw address (id()) across as the 64-bit handle, and the generated code treats it as a PyObject*. On return, a decoder inspects the raw value: small integers and sentinel ranges pass through; list-family handles route through the list bridge; values that look like genuine PyObject* pointers are cast back with ctypes.cast after a header sanity check, with a safe C-string peek as the last resort for string-returning ABIs.
This interop is what lets compiled functions call back into arbitrary Python and still return ordinary Python values to their callers. It is also a real cost centre: every boxed call pays a boundary crossing, a header check and a cast, and the native side loses the optimiser's visibility into whatever happens inside the CPython object. Code that lives mostly in this boxed world compiles, runs correctly, and gains far less than code that stays in typed territory.
Where the runtime is the bottleneck
Three measured patterns limit performance, and all three live here rather than in the compiler.
Allocation churn. The benchmark suite's slower categories — web request handling at 0.42x, database operations at 0.33x, parsing at 0.31x, stdlib-heavy code at 0.27x by geometric mean — share a shape: building and discarding millions of small objects. That workload is bounded by allocation and reference counting, not bytecode dispatch. The native runtime counts references too, as noted above, so compiling such code swaps CPython's allocator traffic for the runtime's own. The worst single result in the suite, 0.01x, came from a particle-movement simulation of exactly this shape. No emission or vectorisation stage can fix a workload whose cost is the data model itself.
Library-bound code. Code that spends its time inside C library routines was never paying bytecode-dispatch tax; there is nothing to win back. Compiling it adds the guard and boundary checks without removing any meaningful work.
Per-call overhead. Guard validation (covered in Guards and deoptimisation) runs on every call in the FFI path, and the argument-marshalling layer walks descriptors each time. For long-running kernels this amortises to nothing; for tiny functions called millions of times, it is a measurable constant added to each call.
The practical guidance follows directly. Choose data structures that stay homogeneous and typed where the shape of your problem allows it; a flat numeric list is a contiguous C array inside, a mixed one is a handle table with tags. When your profile shows allocation dominating, the honest answer is that compilation is not the lever — and the suite's sixteen slower-than-CPython workloads are the published evidence for saying so.
Where to go next
- Native code generation — how the runtime libraries get linked into compiled code, and how SIMD interacts with the typed containers.
- Guards and deoptimisation — the guard layer that validates every call across the ABI.
- Unsupported code and the fallback path — what happens when a function cannot stay on the native side.
- Benchmarks — the measured cost of allocation-heavy and library-bound workloads.
Last reviewed 29 January 2026 against pyvorin-native 1.0.9: library names, the link-order rationale, ABI constants and the boxing mechanism were read from the installed package and its source tree; benchmark figures are from the suite artefact dated 19 July 2026.