Repository navigation
Description
name: Report bugs
about: Report a bug not related to CPython compatibility
title: 'android (bionic): interpreter genesis aborts with "out of TLS keys" — StaticCell thread_local! statics exhaust PTHREAD_KEYS_MAX'
labels: C-bug
assignees: ''
Summary
Embedding rustpython-vm on android (aarch64, bionic libc) without the threading feature aborts during interpreter genesis with:
fatal runtime error: out of TLS keys, aborting
Root cause: StaticCell's non-threading variant is backed by thread_local! statics (LocalKey<OnceCell<...>>). On bionic, each thread_local! static with a destructor consumes one process-wide pthread_key_t, and the pool is capped at PTHREAD_KEYS_MAX = 1024 (shared with every other TLS user in the process, including std and third-party crates).
rustpython-vm instantiates a StaticCell slot per builtin type / exception slot / codec slot via the static_cell! macro — on the order of hundreds of thread_local! statics across crates/vm/src (11 files contain static_cell! invocations; vm/context.rs, exception.rs and the stdlib modules are the big ones). Genesis initializes all of them at once, blowing through the bionic key pool before startup completes. In a real app (game engine embedding), other TLS consumers push the pool close to the limit even before genesis.
Expected
Interpreter genesis completes on android (no-threading configuration), as it does on desktop platforms.
Actual
Genesis aborts during builtin-type initialization:
fatal runtime error: out of TLS keys, aborting
Environment
aarch64-linux-android(bionic), NDK r27+,default-features = false, features:compiler, wasmbind, gc, stdio, importlib, encodings, freeze-stdlib(nothreading, nohost_env)- Verified against current
main:crates/common/src/static_cell.rsnon-threading variant is stillLocalKey<OnceCell<...>>-based (no android-specific branch) - Observed on: RustPython main snapshot 2026-09-22 (
0.6.0-dev), and re-verified the unfixed state on currentmain
Analysis
- Bionic (unlike glibc/musl desktop libs, and unlike macOS/Windows TLS implementations rust uses elsewhere) allocates a
pthread_key_tperthread_local!static with a destructor, from a process-wide pool of 1024 (PTHREAD_KEYS_MAX). - Every
static_cell!slot inrustpython-vmis exactly such a static. Genesis (Context::init_genesis+ stdlib init) touches all of them in one go. - A real application adds its own TLS consumers (std, codecs, GL/math crates), so the effective headroom is smaller than 1024.
Workaround we shipped (for context — not proposed as a direct patch)
We added an android-cfg variant of StaticCell in our vendored copy: a process-global UnsafeCell<OnceCell<T>> + unsafe impl Sync, selected via
#[cfg(all(not(feature = "threading"), feature = "std", target_os = "android"))].
This is safe under our embedding contract (GIL single-thread, one interpreter per process, always driven on the same thread), and genesis + full gameplay now run cleanly on android (aarch64 real device, HarmonyOS NEXT + 卓易通 container).
Caveat: the global-cell approach relies on that single-thread contract and is not thread-compatible — we don't propose it as a direct upstream patch. Upstream would need a thread-compatible design (e.g., one TLS slot holding a registry of all StaticCells, instead of one slot per cell).
Suggested directions
- A key-frugal
StaticCellfor platforms with small pthread key pools (single TLS slot holding a registry/table of cells), selected by cfg ontarget_os = "android"; - Or documenting the android TLS constraint for embedders (no-threading builds abort at genesis once the type count is large enough).
Happy to provide more details, test results, or validate a fix on our android test devices.
Python Documentation
Not applicable — this is a runtime/embedding issue, not a CPython compatibility issue.
Activity