pythonrs stdlib via CPython FFI
September 5, 2026 · View on GitHub
Decision: pythonrs does NOT reimplement the stdlib. It imports the real CPython
stdlib — pure .py and C-accelerator .so modules — over an FFI bridge to an
embedded libpython. User code runs on fusevm (JIT/rkyv/AOT); import <stdlib>
delegates to CPython.
Validated (isolated spike — proven, do not re-litigate)
- pyo3 0.24 with feature
abi3-py39builds/links against CPython 3.10, 3.12 and 3.14 via the stable ABI, with noPYO3_USE_ABI3_FORWARD_COMPATIBILITYand no other extra env. The floor wasabi3-py313, which brokecargo install pythonrson hosts with an older CPython and simultaneously sat on pyo3 0.24's own 3.13 ceiling (hence the forward-compat flag). 3.9 is as low as the limited API goes here:#[pyclass(dict)]insrc/ffi.rsis rejected underabi3-py38. - Import sweep: 61/61 modules load — pure (
argparse csv textwrap dataclasses enum pathlib json logging http email xml…) and C-accel (re/_sre hashlib/_hashlib datetime/_datetime socket/_socket struct math random pickle/_pickle base64/binascii zlib itertools). C code runs, results marshal back to Rust (bytes/list/tuple/dict/ int/float/str):hashlib.sha256(b"abc")→correct,Decimal("0.1")+Decimal("0.2")→0.3exact,struct.pack(">I",1000)→[0,0,3,232],pickleroundtrip,argparseparse. - Stdlib resolution proven both ways via
PYTHONHOME/sys.prefix(set before init):- system: no override → uses installed CPython's
Lib/. - bundled:
PYTHONHOME=<bundle>→ loads<bundle>/lib/python3.14/+lib-dynload/.
- system: no override → uses installed CPython's
Implementation (feature-gated so it never breaks default/peer builds)
-
Cargo — optional dep, feature ON by default:
[dependencies] pyo3 = { version = "0.24", features = ["abi3-py39", "auto-initialize"], optional = true } [features] default = ["stdlib-ffi"] stdlib-ffi = ["dep:pyo3"]A bare
cargo build/test/clippylinks libpython and imports the real stdlib with no extra env on any CPython 3.9–3.14 (PYO3_PYTHONis needed only whenpython3onPATHis pythonrs itself, which pyo3 cannot use). A pyo3-free/libpython-free build usescargo build --no-default-features. -
src/ffi.rs(#[cfg(feature = "stdlib-ffi")]):init()once at startup: resolve the stdlib prefix (order:PYTHONRS_STDLIBenv → bundled<exe_dir>/../lib/python3.14→ system CPython → error), setPyConfig.home/PYTHONHOMEbeforePy_Initialize.import(name) -> Result<ForeignHandle, String>:Python::with_gil(|py| py.import(name)), store thePy<PyAny>in a host side-table, return an id. Handles are memoized by module name:sys.moduleshands back the SAME object on every import, so storing it again would only grow the table — and the native-shadow fallback re-imports on every attribute miss (math.isqrt,collections.ChainMap), as does each thread's own host module cache.- Marshal helpers: pythonrs
Value↔ CPython object. By value in both directions for int/float/bool/None/str/bytes/list/tuple/dict/set, plus (in) a bytearray→CPythonbytearray, range, complex,collections.deque, and frozenset. By handle (PyObj::Foreign) for everything else (compiled regex, datetime, socket, file, …). In-place mutation write-back: after a call, a by-value mutable-container argument (list/bytearray/deque) is re-read from its CPython object and the pythonrs heap slot is overwritten in place, so in-place stdlib mutators (heapq.heapify,random.shuffle,struct.pack_into) reflect back and aliases observe them. Write-back marshals by value only (never allocates aForeign), so it does not grow the side-table. - Handle lifetime (known limit): the side-table is bounded for the
value-marshaled path but not reclaimed for stdlib calls that return a live
CPython object (
re.matchresults, datetime, files) — each takes a permanent slot, growing 1:1 with the pythonrs host heap. The host heap is an arena that never frees any object andPyObj::Foreigncarries only a bare id, so the bridge has no drop signal and cannot safely reclaim. Real reclamation needs aForeign-drop callback / arena GC inhost.rs(out of the bridge's scope).
-
PyObj::Foreign(u32)(#[cfg(feature)]variant → id into the ffi side-table). Routeget_attr/call/__getitem__/__iter__/__next__/str/repr/len/__contains__on a Foreign through pyo3 (marshal args in, result out). pyo3 owns refcounts + the GIL. Add#[cfg(feature)]arms to the PyObj matches (type_name, str_of, repr_of, truthy, get_attr, dispatch, invoke). Binary / comparison / unary operators on a Foreign operand (+ - * / // % ** @ & | ^ << >>,== != < <= > >=, unary- + ~ abs) route throughffi::binary_op/unary_op, which marshal both operands (a native operand crosses by value) and call CPython'soperator.<fn>; the result marshals back by value or as a freshForeign. Minimal#[cfg(feature)]hooks live at the top ofPyHost::arith(+ - *, comparisons, unary-),PyHost::binop(/ // % ** @ & | ^ << >>),PyHost::unary(~, unary+), and theabsbuiltin. A CPythonTypeError/NotImplementedsurfaces as a pythonrs error, never a panic. -
host::import_module— on the current miss (beforeModuleNotFoundError), ifstdlib-ffi, tryffi::import(name)→ wrap as aModulewhose attrs are Foreign proxies (or a Foreign module handle).from x import y, submodules (os.path),sys.modulesall fall out of CPython's own importer. -
Delete the remaining hand-rolled shadows — DONE.
src/stdlib/{json,os,random, string,itertools,functools,statistics,textwrap}.rsare gone (as are the earlierre/datetime/heapq/bisect), along with theirimport_module/call_builtin_function/is_builtin_functionwiring. What remains undersrc/stdlib/is the genuinely-native set the bridge does not serve:binascii codecs pyast pycsv pyhash pyimp pyio pyopcode pysignal pystruct pythread pytokenize.sysstays wholly native (itsargv/exit/stdoutare fusevm-runtime objects, deliberately never deferred), whilemath,collections,functools, andcontextlibresolve their native arms first and defer to CPython only on a miss (module_ffi_fallback,src/host.rs). -
Bundle packaging (the "install stdlib with it" path) — DONE via
scripts/install.sh, which installs a fully self-contained runtime into~/.pythonrs(co-located with the bytecode cache):~/.pythonrs/bin/python the pythonrs binary ~/.pythonrs/lib/libpython3.14.dylib the CPython runtime ~/.pythonrs/lib/lib{crypto,ssl,sqlite3,lzma,zstd,mpdec}… C-ext deps ~/.pythonrs/lib/python3.14/ pure stdlib + lib-dynload/*.soffi::resolve_home()finds it (via<exe>/../libor a~/.pythonrsfallback) and pinsPYTHONHOMEbeforePy_Initialize. Crucially the installer does a recursive relink: it copies EVERY non-system dylib the runtime touches — libpython AND the C-extensions' transitive Homebrew deps (openssl, sqlite, xz, zstd, mpdecimal) — intolib/, rewrites every load command to@rpath, adds the matching rpath, and ad-hoc re-signs (arm64 dyld rejects an invalid signature). The result has zero/opt/homebrewreferences, sobrew uninstall python(and those five formulae) leaves pythonrs running. Verified: the vendored binary loads~/.pythonrs/lib/libpython3.14.dyliband importshashlib/ssl/sqlite3/lzma/decimal/json/… with nothing under/opt/homebrewreferenced. Put~/.pythonrs/binonPATH(or symlinkbin/python— a barecpbreaks the@executable_pathrpath).scripts/bundle-stdlib.shstill stages the olderdist/<triple>release-tarball layout but only relinks the binary (its C-extension transitive deps are NOT yet vendored — useinstall.shfor a truly Homebrew-free tree). Caveat: this vendors the RUNTIME. Rebuilding pythonrs from source still needspython@3.14present (pyo3 links it at build time); runtime is independent.
Language gaps once tracked here — all landed
Exception chaining (__cause__/__context__, raise X from Y — set_exc_link
in src/host.rs); lazy zip/map/filter/enumerate (real lazy iterator
objects) plus infinite-islice via the bridge; frozenset as a real type
(PyObj::Frozenset); dict-view set-ops and the range/set method surface;
slice assignment and del (set_slice_vals/del_slice); the remaining str
methods (casefold/swapcase/title/expandtabs/rpartition/removeprefix/
removesuffix/isprintable/…); repr control-char escaping ('a\tb\nc\x00d');
positional-only enforcement (FnDef::posonly); metaclasses (__prepare__ +
types.new_class over the bridge). Earlier: complex arithmetic, super/C3,
property/descriptors, the iteration protocol, generator send/throw/close,
banker's rounding, bignum, numeric-key unification.
BUGS.md — not this file — is the live ledger of what is still missing.