Skip to content

Commit 7e8a923

Browse files
fix(android): make PEP 734 subinterpreters work (concurrent.interpreters / InterpreterPoolExecutor) (#239)
* fix(android): make subinterpreters able to import relocated native modules PEP 734 subinterpreters (Python 3.14 concurrent.interpreters / InterpreterPoolExecutor) were unusable in Android apps: the main interpreter works, but every subinterpreter failed to import any C extension (ModuleNotFoundError: _struct / _interpqueues / ...), which breaks the whole feature (its cross-interpreter transport pickles -> _struct, and its queues need _interpqueues). iOS and desktop are unaffected. Root cause: Android relocates C extensions (native-mmap packaging) and resolves them through the custom _SorefFinder installed on sys.meta_path. sys.meta_path is per-interpreter, and install() only ran in the main interpreter, so a freshly created subinterpreter had a meta_path without the finder and could import no relocated .so. Verified on-device: main meta_path has _SorefFinder, a subinterpreter's does not. Fix: teach every new subinterpreter to install the finder. Split the finder insertion into _install_finder(), and make install() also call a new _patch_subinterpreters() that wraps concurrent.interpreters.create() so each new interpreter runs `import _sp_bootstrap; _sp_bootstrap.install()`. The setup runs via Interpreter.exec() -- a source string, which is pickle-free (unlike Interpreter.call()), so it works BEFORE _struct is importable in the child. (A fresh subinterpreter inherits the process-wide sys.path, so no sys.path seeding is needed -- verified on-device.) Notes: - No dart-bridge / C change: the existing Android bootstrap already calls install(), which now triggers the patch. The wrapper is tagged `_sp_patched` for idempotency. - create() is the factory InterpreterPoolExecutor uses (WorkerContext.initialize -> interpreters.create()), so patching it fixes the pool transparently -- with no user code change. - A user-supplied InterpreterPoolExecutor(initializer=...) can NOT fix this: the initializer is delivered over the same broken pickle path, so the finder must be installed at interpreter-creation time. - No-op before 3.14 (module absent) and on iOS/desktop (this file is Android-only). Idempotent; supports nested subinterpreters. Verified end-to-end on an Android 15 / arm64-v8a emulator (Python 3.14.6): the clean probe app -- with no app-level workaround -- runs a low-level create+queue round-trip, an InterpreterPoolExecutor across 4 interpreters in one process, and imports a C extension inside a subinterpreter. * docs: note the Android subinterpreter fix under 4.3.6 Fold the subinterpreter fix into the existing 4.3.6 section (main jumped 4.3.4 -> 4.3.6 with the Windows UTF-8 release; our 4.3.5 draft was rebased away): a detailed bullet in serious_python_android and a summary bullet in serious_python (umbrella). The Apple/Windows/Linux/ interface packages already carry their 4.3.6 entries from the Windows release and are unaffected by this Android-only change. No pubspec bump needed — this rides in the same 4.3.6 that main is already on. * docs+debug: note create()-attribute assumption; stderr breadcrumb on child finder-install failure --------- Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
1 parent daada1a commit 7e8a923

3 files changed

Lines changed: 64 additions & 1 deletion

File tree

src/serious_python/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 4.3.6
22

3+
* **Android:** PEP 734 subinterpreters (Python 3.14's `concurrent.interpreters` / `InterpreterPoolExecutor`) now work in built apps. Previously the main interpreter could import them, but every *subinterpreter* failed to import any relocated C extension (`ModuleNotFoundError: _struct` / `_interpqueues` / ...) — which broke the whole feature (its cross-interpreter transport pickles → `_struct`, and its queues need `_interpqueues`). The native-module finder lives on `sys.meta_path`, which is per-interpreter, and was installed only in the main interpreter. See `serious_python_android` 4.3.6.
34
* **Windows:** fix startup with non-ASCII app paths or environment values — Dart FFI strings (UTF-8) are now converted to UTF-16 before being passed to the Windows CRT, so paths/env values are no longer corrupted through the process ANSI code page, and Python UTF-8 mode is enabled before `Py_Initialize()`. See `serious_python_windows` 4.3.6 and flet-dev/flet#6641.
45
* Bundled python-build snapshot re-pinned to **20260720** (`dart_bridge` **1.5.0 → 1.5.1**, which carries the Windows fix above). Python versions (**3.12.13 / 3.13.14 / 3.14.6**) are unchanged.
56

src/serious_python_android/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 4.3.6
22

3+
* **PEP 734 subinterpreters now work** (Python 3.14 `concurrent.interpreters` / `InterpreterPoolExecutor`) — enabling true multi-core CPU parallelism inside a single process, which `multiprocessing` cannot provide on Android. Previously the main interpreter imported the machinery fine, but every subinterpreter raised `ModuleNotFoundError` for `_struct` / `_interpqueues` / any other C extension, so `InterpreterPoolExecutor` and the low-level API were unusable. Cause: Android relocates C extensions (native-mmap packaging) and resolves them through `_SorefFinder` on `sys.meta_path`; `sys.meta_path` is per-interpreter, and `install()` only ran in the main interpreter, so a freshly created subinterpreter had a `meta_path` without the finder. Fix: `_sp_bootstrap.install()` now also wraps `concurrent.interpreters.create()` so every new interpreter installs the finder before use. The install runs via `Interpreter.exec()` — a source string, which is pickle-free (unlike `Interpreter.call()`), so it works *before* `_struct` is importable in the child. Because `InterpreterPoolExecutor` builds its workers via `concurrent.interpreters.create()`, the pool is fixed transparently with no user-code change. A no-op before 3.14; iOS/desktop were unaffected.
34
* Re-pins the bundled python-build snapshot to **20260720** (`dart_bridge` **1.5.0 → 1.5.1**). 1.5.1 is a Windows-only UTF-8 startup fix (see `serious_python_windows` 4.3.6); the Android runtime is functionally unchanged from 20260719.
45

56
## 4.3.4

src/serious_python_android/python/_sp_bootstrap.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def find_spec(self, fullname, path=None, target=None):
136136
return spec
137137

138138

139-
def install():
139+
def _install_finder():
140140
"""Insert the finder at the front of `sys.meta_path` (idempotent)."""
141141
global _installed
142142
if _installed:
@@ -158,3 +158,64 @@ def install():
158158
return
159159
sys.meta_path.insert(0, _SorefFinder())
160160
_installed = True
161+
162+
163+
def _patch_subinterpreters():
164+
"""Make PEP 734 subinterpreters (Python 3.14+) able to import relocated
165+
native modules.
166+
167+
`sys.meta_path` is *per-interpreter*: a subinterpreter created by
168+
`concurrent.interpreters` / `InterpreterPoolExecutor` starts with a fresh
169+
meta_path that does NOT contain the `_SorefFinder` installed by `_install_finder`,
170+
so it can import no relocated C extension (`_struct`, `_interpqueues`, ...). Since
171+
that machinery ships work between interpreters via pickle (`_struct`) and
172+
cross-interpreter queues (`_interpqueues`), the whole feature is unusable.
173+
174+
Wrap `concurrent.interpreters.create()` so every new interpreter installs
175+
the finder before it is used. The install runs via `Interpreter.exec()` — a
176+
source string, which is pickle-free (unlike `Interpreter.call()`), so it
177+
works *before* `_struct` is importable in the child. Idempotent; a no-op
178+
before 3.14 (module absent) and cannot fix
179+
`InterpreterPoolExecutor(initializer=...)` because the initializer is
180+
delivered over the same broken pickle path — so it must run here, at
181+
creation time.
182+
183+
NOTE: this relies on callers resolving `interpreters.create` as a module
184+
attribute at call time (as `InterpreterPoolExecutor` does on 3.14) rather
185+
than binding it via `from concurrent.interpreters import create`. If a
186+
future CPython changes that, the patch silently stops applying to the pool
187+
and subinterpreter imports regress — verified working on 3.14.6.
188+
"""
189+
try:
190+
from concurrent import interpreters
191+
except Exception:
192+
return # < 3.14, or subinterpreters unavailable
193+
if getattr(interpreters.create, "_sp_patched", False):
194+
return
195+
_orig_create = interpreters.create
196+
197+
def create(*args, **kwargs):
198+
interp = _orig_create(*args, **kwargs)
199+
try:
200+
interp.exec("import _sp_bootstrap\n_sp_bootstrap.install()\n")
201+
except Exception as e:
202+
# Finder not installed in the child: it will hit the original
203+
# ModuleNotFoundError on its first relocated import. Leave a
204+
# breadcrumb (cf. the SP_BOOTSTRAP audit in `install`) instead of
205+
# failing `create` outright.
206+
sys.stderr.write("SP_BOOTSTRAP subinterpreter finder install failed: %r\n" % (e,))
207+
return interp
208+
209+
create._sp_patched = True
210+
interpreters.create = create
211+
212+
213+
def install():
214+
"""Entry point called from the dart-bridge Android bootstrap.
215+
216+
Installs the native-module finder in the current interpreter and — on
217+
3.14+ — teaches every future subinterpreter to install it too.
218+
Safe to call in any interpreter; idempotent.
219+
"""
220+
_install_finder()
221+
_patch_subinterpreters()

0 commit comments

Comments
 (0)