Skip to content

refactor: Pass module paths and sys.argv to CPython as objects - #18

Open
ndonkoHenri wants to merge 3 commits into
fix/pth-site-dirsfrom
refactor/bootstrap-object-injection
Open

refactor: Pass module paths and sys.argv to CPython as objects#18
ndonkoHenri wants to merge 3 commits into
fix/pth-site-dirsfrom
refactor/bootstrap-object-injection

Conversation

@ndonkoHenri

Copy link
Copy Markdown
Contributor

Based/stacked on #17

Why rewrite working code

sp_apply_module_paths and sp_apply_program_name passed data (module paths, the program name) into the interpreter by rendering it into Python source text — escaping each value into a quoted literal inside a hand-sized buffer, then evaluating the result. Anything traveling through the code channel needs quoting, and the quoting layer is where the bugs were:

  • a module path ending in a backslash produced a SyntaxError (a raw string cannot end in one) — app fails to start;
  • a module path containing a newline broke the generated source — same;
  • a program name containing ''' broke the r'''…''' literal — same;
  • a program name longer than the 1024-byte format buffer got truncated mid-statement, cutting off the closing quotes — a SyntaxError at boot, not a truncated value;
  • an apostrophe-dense path could overflow the original buffer estimate (patched in the base branch; that patch is deleted here because the machinery it fixed is gone, not lost).

What this does instead

Module paths (commit 1): the list is built with the C API (PyList_New / PyUnicode_FromString / PyList_SetItem, all abi3) and exposed as _sp_paths in a private globals dict wired to the interpreter's builtins; the fixed bootstrap script (dedupe + site.addsitedir, unchanged logic from the base branch) runs against those globals. Data is objects end to end — nothing is escaped, and the estimate/escape/guard machinery is deleted. Private globals also mean the script's imports and temporaries cannot touch the user program's __main__, so its cleanup del is gone and __main__ is left exactly as before this change (no observable behavior difference). sp_pyrun_string's compile+eval core is factored out as sp_pyrun_string_in_globals.

sys.argv (commit 2): same treatment — one-element list via the C API, installed with PySys_SetObject("argv", …). Identical end state, no quoting, no length limit; the "python" default is unchanged.

Verification

  • All APIs used are confirmed in the Limited API at 0x030c0000 three ways: guard-compile, the vendored 3.14 headers' guards, and symbol presence in the Windows abi3 stub (python3.lib). PyEval_GetBuiltins is the only builtins accessor at this level (PyEval_GetFrameBuiltins is 3.13+).
  • A local harness links the actual sources and drives serious_python_run() end to end against an embedded interpreter (3.12 and 3.14), mirroring the real plugins' env (PYTHONPATH duplication, LC_CTYPE=UTF-8). Scenarios, all passing:
    1. .pth path-line + import-line processing, PYTHONPATH dedupe, precedence order, apostrophe / trailing-backslash / newline / non-ASCII dirs, a non-directory modules.zip entry (Android shape), and __main__-globals cleanliness;
    2. zero module paths (bootstrap skipped, clean exit);
    3. argv: default name, '''-containing name, 3000-byte name;
    4. negative: invalid-UTF-8 path → clean rc=1 with the [serious_python_run] stderr marker, no crash.
  • The pywin32 repro for ModuleNotFoundError: No module named 'win32com' flet#5071 was previously verified on a Windows 11 VM against the base branch's DLL; this PR keeps that behavior (the bootstrap script's logic is unchanged) — re-verified locally via the same .pth fixture.

sp_apply_module_paths rendered the path list into Python source text —
escaping each path into a quoted literal inside a hand-sized buffer —
and evaluated the result against __main__'s globals. Data traveling
through the code channel needs quoting, and the quoting layer is where
the bugs lived: an apostrophe-dense path could overflow the original
buffer estimate, a path ending in a backslash produced a SyntaxError (a
raw string cannot end in one), and a path containing a newline broke
the generated source.

Build the list with the C API instead (PyList_New /
PyUnicode_FromString / PyList_SetItem, all abi3), expose it as
`_sp_paths` in a private globals dict wired to the interpreter's
builtins, and run the fixed bootstrap script there (the former
SP_MODULE_PATHS_EPILOGUE, now SP_MODULE_PATHS_SCRIPT). Paths are
objects end to end, so nothing is escaped and the whole
estimate/escape/guard machinery is deleted. Private globals mean the
script's imports and temporaries cannot collide with the user program's
namespace, so the script's cleanup `del` goes away too — and __main__'s
globals are left exactly as before this change.

sp_pyrun_string's compile+eval core is factored out as
sp_pyrun_string_in_globals (the __main__ lookup gains the missing NULL
check on PyModule_GetDict). Failures surface an explicit error at each
step — including a RuntimeError if builtins are unavailable — before
the shared sp_paths_failed tail reports on stderr.
Same treatment as the module paths: sp_apply_program_name formatted the
program name into "import sys; sys.argv = [r'''name''']". Any name
containing ''' broke the generated statement, and names that pushed it
past the 1024-byte snprintf buffer broke startup — the truncation cut
the closing quotes off the statement, producing a SyntaxError at boot
rather than a truncated value. Build the one-element list with the C
API and install it with PySys_SetObject("argv", ...) — identical end
state, no quoting, no length limit. The default "python" fallback is
unchanged, and `sys` in __main__ is untouched (the stdio redirect
already provides that binding, as before).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant