◐ Shell
reader mode source ↗
Skip to content

Add c-api error handling#7784

Closed
bschoenmaeckers wants to merge 12 commits into
RustPython:mainfrom
bschoenmaeckers:capi-errors
Closed

Add c-api error handling#7784
bschoenmaeckers wants to merge 12 commits into
RustPython:mainfrom
bschoenmaeckers:capi-errors

Conversation

@bschoenmaeckers

@bschoenmaeckers bschoenmaeckers commented May 5, 2026

Copy link
Copy Markdown
Contributor

This is the next step in adding c-api support. It also contains utilities to handle errors for implementing other c-api functions.

Summary by CodeRabbit

  • New Features

    • Added optional C API support enabling RustPython to function as a Python interpreter through C FFI bindings, compatible with existing C extensions and libraries expecting standard Python C interfaces.
  • Refactor

    • Refactored thread-local VM tracking mechanism to use a stack-based approach.
    • Tightened conditional compilation requirements for fork-related functionality to require host environment support.
  • Chores

    • Updated spell-check dictionary entries.
    • Removed unused threading dependency.

@bschoenmaeckers bschoenmaeckers changed the title ADD c-api error handling May 5, 2026
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces a Rust-to-C FFI binding layer (rustpython-capi crate) exposing core RustPython interpreter lifecycle, exception, object, and state management operations. It refactors VM thread management from scoped TLS to stack-based attachment, adds conditional fork-safety gates behind a host_env feature, and wires the C-API through Cargo features and build configuration.

Changes

C-API Layer Integration

Layer / File(s) Summary
Feature & Build Configuration
Cargo.toml, build.rs, .cspell.dict/*
New optional capi feature added to top-level crate with rustpython-capi workspace dependency; linker export-dynamic flags gated behind CARGO_FEATURE_CAPI on Linux/macOS; spell-check dictionaries updated with pystate and PYGILSTATE.
VM Thread Management Foundation
crates/vm/src/vm/thread.rs
Replaced scoped_thread_local! VM tracking with stack-based VM_STACK (RefCell) and added per-thread GILSTATE_VM storage; introduced CurrentVmAttachState enum and public attach_current_thread() / release_current_thread() APIs for external thread attachment; adjusted Unix stop-the-world transitions to use new stack mechanism via set_current_vm().
Exception API Visibility
crates/vm/src/vm/mod.rs
Exposed current_exception() and set_exception() from pub(crate) to pub; added take_raised_exception() to extract top exception with thread state updates.
C-API Crate Infrastructure
crates/capi/Cargo.toml, crates/capi/.cargo/config.toml, crates/capi/pyo3-rustpython.config, crates/capi/src/lib.rs
New rustpython-capi crate configured as both cdylib and rlib; pyo3 dev-dependency with abi3 feature; environment config for PYO3_CONFIG_FILE and PYO3_NO_PYTHON; module surface declares object, pyerrors, pylifecycle, pystate, refcount modules; added get_main_interpreter() and set_main_interpreter() for process-wide interpreter access.
FFI Result Abstraction
crates/capi/src/util.rs
Introduced FfiResult<Output> trait with error sentinel (ERR_VALUE) and into_output() conversion; implementations for unit, primitives, raw pointers, and PyResult<T> enable consistent C-ABI boundary error handling.
C-API Implementations
crates/capi/src/pylifecycle.rs, crates/capi/src/pystate.rs, crates/capi/src/object.rs, crates/capi/src/pyerrors.rs, crates/capi/src/refcount.rs
Implemented Python C-API entry points: Py_Initialize/Py_Finalize manage interpreter lifecycle via MAIN_INTERP global; PyGILState_Ensure/Release manage thread attachment; Py_TYPE, Py_IS_TYPE, PyType_GetFlags provide object/type introspection; exception zoo statics and functions (PyErr_Occurred, PyErr_SetRaisedException, PyErr_NewException, etc.) handle exception lifecycle; _Py_IncRef/_Py_DecRef manage reference counts.
Integration & Feature Gating
src/lib.rs
Conditional execution path under feature = "capi" initializes main interpreter via rustpython_capi, sets it before running, and finalizes after completion; non-capi builds use original interp.run() path.
Fork-Safety Conditioning
crates/vm/src/builtins/type.rs, crates/vm/src/codecs.rs, crates/vm/src/intern.rs, crates/vm/src/object/core.rs, crates/vm/src/stdlib/_imp.rs, crates/vm/src/stdlib/_io.rs, crates/vm/src/stdlib/_thread.rs
Fork-related reinitialization helpers now require feature = "host_env" in addition to feature = "threading"; affects reinit_after_fork methods, weakref lock reset, import lock management, and std-stream reinitialization to prevent compilation when C-API host-environment integration is not intended.
Dependency Cleanup
crates/vm/Cargo.toml
Removed unused scoped-tls dependency; thread tracking now uses stack-based thread_local instead.

Sequence Diagram

sequenceDiagram
    participant Client as External C Code
    participant API as RustPython C-API
    participant Interp as Interpreter<br/>(MAIN_INTERP)
    participant VM as VirtualMachine
    participant Exc as Exception Stack

    Client->>API: Py_Initialize()
    API->>Interp: create & store MAIN_INTERP
    Interp->>VM: Interpreter::with_init()
    Interp->>VM: ensure_thread_has_vm_attached()
    
    Client->>API: PyGILState_Ensure()
    API->>VM: attach_current_thread()
    VM-->>API: return attached state

    Client->>API: PyErr_SetString(PyExc_TypeError, msg)
    API->>Exc: set exception in VM
    Exc-->>API: exception stored

    Client->>API: PyErr_Occurred()
    Exc-->>API: return exception ptr

    Client->>API: PyGILState_Release(state)
    API->>VM: release_current_thread()
    VM-->>API: detached

    Client->>API: Py_FinalizeEx()
    API->>Interp: finalize main interpreter
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • RustPython/RustPython#7648: Both PRs add the same C-API infrastructure (crates/capi crate, pylifecycle/pystate/refcount modules, Cargo.toml wiring, build.rs linker flags) with identical file structure and symbols.
  • RustPython/RustPython#7166: Related at the VM/exception level — both PRs modify exception stack storage and thread-local exception state handling, with this PR exposing public exception APIs that depend on refactored exception management.

Suggested Reviewers

  • ShaharNaveh
  • youknowone

Poem

🐰 A C-bridge now spans the gulf so deep,
Where RustPython's VM treasures sleep,
Initialize, attach threads with glee,
Reference counts dance merrily,
Safe FFI boundaries firmly weep! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Add c-api error handling' partially relates to the changeset but is overly broad and vague. The PR introduces extensive C-API infrastructure including object types, lifecycle management, thread state, reference counting, and more—not just error handling. While error handling is present (pyerrors.rs), it represents only one aspect of a much larger C-API implementation effort. Consider a more specific title such as 'Implement Python C-API bindings and infrastructure' or 'Add comprehensive C-API layer with object, lifecycle, and thread management' to better reflect the full scope of changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hide comment

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/vm/src/object/core.rs (1)

477-503: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Gate narrowing on reset_weakref_locks_after_fork breaks its call site in posix.rs::py_os_after_fork_child

reset_weakref_locks_after_fork (line 500) is now compiled only under unix + threading + host_env, but its call site in crates/vm/src/stdlib/posix.rs::py_os_after_fork_child (line ~791) is gated only on #[cfg(feature = "threading")] (and posix.rs is Unix-only, making it effectively unix + threading). Builds with unix + threading but without host_env will fail to compile there.

The call in posix.rs:

#[cfg(feature = "threading")]
crate::object::reset_weakref_locks_after_fork();

needs to become:

#[cfg(all(feature = "threading", feature = "host_env"))]
crate::object::reset_weakref_locks_after_fork();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm/src/object/core.rs` around lines 477 - 503, The call site gating
for reset_weakref_locks_after_fork in posix.rs::py_os_after_fork_child must
match the function's cfg; change the cfg on the call to require both feature =
"threading" and feature = "host_env" (i.e. use #[cfg(all(feature = "threading",
feature = "host_env"))]) so the call to
crate::object::reset_weakref_locks_after_fork() is only compiled when the
function is present.
crates/vm/src/vm/mod.rs (1)

2063-2098: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Back C-API exception functions with a separate error-indicator state, not the handled-exception stack.

Currently, PyErr_Occurred() and PyErr_GetRaisedException() read from the same stack that backs sys.exc_info() and generator gi_exc_state (the exc_info slot managed by PUSH_EXC_INFO/POP_EXCEPT). According to CPython semantics, these C-API functions should operate on the per-thread "error indicator" state—representing exceptions still propagating between C calls—which is distinct from and independent of the handled-exception state. Using the exc_info stack causes:

  1. C-API code can observe exceptions caught by Python try/except, which should be invisible at the C level.
  2. Calling PyErr_GetRaisedException() + PyErr_SetRaisedException() can corrupt the active exception state of the current frame or generator.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm/src/vm/mod.rs` around lines 2063 - 2098, The C-API error-indicator
must be backed by a separate per-Vm state, not the exc_info stack used by
sys.exc_info()/PUSH_EXC_INFO; change the methods so they don't read/write
exceptions.stack: leave current_exception/set_exception (and their semantics for
the exc_info stack) as-is, and introduce/replace the C-API facing API with a
separate storage (e.g., add a new field like raised_exception:
RefCell<Option<PyBaseExceptionRef>>) and implement get_raised_exception /
set_raised_exception / take_raised_exception to operate on that field instead of
exceptions.stack; ensure thread::update_thread_exception continues to reflect
the thread-visible error indicator using the new getter (not topmost_exception),
and update any callers that relied on the old take_raised_exception to use the
new C-API methods so C-level errors do not corrupt the handled-exception stack.
🧹 Nitpick comments (5)
crates/capi/src/pystate.rs (1)

11-13: 💤 Low value

Caveat for future callers of with_vm.

with_current_vm holds a RefCell borrow on VM_STACK for the duration of the callback (per crates/vm/src/vm/thread.rs:88-99). If f ever transitively calls release_current_thread or anything that tries to mutate VM_STACK, it will hit a borrow_mut panic. None of today's call sites do that, but a brief safety comment on with_vm would prevent future footguns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/pystate.rs` around lines 11 - 13, with_vm currently calls
with_current_vm which holds a RefCell borrow on VM_STACK for the duration of the
callback; if the provided closure f (or anything it transitively calls) tries to
mutate VM_STACK (for example by calling release_current_thread) it will cause a
borrow_mut panic — add a concise safety comment on with_vm explaining that the
closure must not call any function that mutates or re-enters VM_STACK (e.g.,
release_current_thread or other VM stack-mutating APIs) and suggest using a
different API or refactoring to avoid holding the borrow across such calls;
reference the functions with_vm and with_current_vm and the VM_STACK cell in the
comment to help future callers locate the caveat.
crates/capi/src/util.rs (1)

79-85: 💤 Low value

usize → isize cast can produce a spurious error sentinel.

self as isize wraps when self > isize::MAX, which would silently turn a very large length into a negative value indistinguishable from ERR_VALUE = -1. For sizes returned by C-API functions (e.g., Py_ssize_t-shaped APIs) this is unlikely in practice, but consider isize::try_from(self).unwrap_or(isize::MAX) or asserting the upper bound, or at minimum a comment documenting the assumption.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/util.rs` around lines 79 - 85, The impl FfiResult<isize> for
usize currently casts usize -> isize in into_output which can wrap for values >
isize::MAX and collide with ERR_VALUE; change into_output in the impl for usize
to convert safely (e.g., use isize::try_from(self).unwrap_or(isize::MAX) or
otherwise saturate/validate and return isize::MAX on overflow) and document the
chosen behavior, making sure ERR_VALUE (-1) remains distinct; update the impl
block (symbols: FfiResult<isize> for usize, const ERR_VALUE, fn
into_output(self, _vm: &VirtualMachine)) accordingly.
crates/capi/src/object.rs (1)

70-83: ⚡ Quick win

Replace panic! with CPython-conventional error.

A panic! in an extern "C" function aborts the process (since Rust 1.81 panics across the FFI boundary unwind into an abort). CPython's Py_GetConstant/Py_GetConstantBorrowed conventionally raise SystemError and return NULL on an invalid id, which is friendlier and matches what C-extension authors expect.

♻️ Suggested change
-pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject {
-    with_vm(|vm| {
-        let ctx = &vm.ctx;
-        match constant_id {
-            0 => ctx.none.as_object(),
-            1 => ctx.false_value.as_object(),
-            2 => ctx.true_value.as_object(),
-            3 => ctx.ellipsis.as_object(),
-            4 => ctx.not_implemented.as_object(),
-            _ => panic!("Invalid constant_id passed to Py_GetConstantBorrowed"),
-        }
-        .as_raw()
-    })
-}
+pub extern "C" fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject {
+    with_vm(|vm| -> PyResult<PyObjectRef> {
+        let ctx = &vm.ctx;
+        let obj = match constant_id {
+            0 => ctx.none.as_object(),
+            1 => ctx.false_value.as_object(),
+            2 => ctx.true_value.as_object(),
+            3 => ctx.ellipsis.as_object(),
+            4 => ctx.not_implemented.as_object(),
+            _ => return Err(vm.new_system_error(format!(
+                "Py_GetConstantBorrowed: invalid constant_id {constant_id}"
+            ))),
+        };
+        Ok(obj.to_owned())
+    })
+}

(Or, if the as_raw/borrowed-pointer semantics need to be preserved exactly, return *mut PyObject directly via FfiResult and set the exception inline, returning null_mut() on the bad-id path.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/object.rs` around lines 70 - 83, In Py_GetConstantBorrowed
replace the panic! on the _ (invalid id) branch with CPython-style behavior:
create/raise a SystemError on the VM/context (e.g. via
vm.ctx.new_system_error(...) or the crate's VM exception-raising helper) and
return null_mut() from the extern "C" function; alternatively, change the
function to return an FfiResult that sets the exception and yields a null
pointer on error. Ensure you still return the borrowed object's as_raw() for
valid ids and do not unwind across the FFI boundary.
crates/capi/src/lib.rs (1)

27-33: 💤 Low value

Context::genesis() is reused; verify it matches the interpreter's context.

init_exception_statics(&Context::genesis().exceptions) initializes exception statics from the genesis context, while the stored interpreter may have been built with its own Context. Today genesis() is effectively a global singleton in RustPython so this works, but if Context ever becomes per-interpreter, the statics here will diverge from the interpreter actually serving C-API calls. Consider deriving the exceptions from the interpreter being installed (e.g., interpreter.enter(|vm| init_exception_statics(&vm.ctx.exceptions)) before storing) so the wiring stays correct under future refactors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/capi/src/lib.rs` around lines 27 - 33, The code currently calls
init_exception_statics(&Context::genesis().exceptions) which may diverge if
Context becomes per-interpreter; change set_main_interpreter to derive
exceptions from the Interpreter being installed by entering the interpreter
(e.g., use Interpreter::enter or interpreter.enter(|vm| ...)) and call
init_exception_statics(&vm.ctx.exceptions) while still in that closure, then
store the interpreter; ensure the safety comment is updated to reflect that
exception statics are initialized from the interpreter's Context and keep the
assert!(interp.is_none()) and assignment to *interp = Some(interpreter) as
before.
build.rs (1)

5-12: 💤 Low value

Windows + capi has no symbol-export configuration.

When target == "windows" && capi_enabled, the binary won't export the C-API symbols (no --export-all-symbols, no .def file, no __declspec(dllexport) on the Rust side). Linux/macOS get -Wl,--export-dynamic/-Wl,-export_dynamic, but Windows binaries need explicit export markup to be loadable as a Python host by C extensions. If Windows support for capi is intended, consider emitting an equivalent flag or .def file; if it's deliberately deferred, a comment here documenting that would help.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.rs` around lines 5 - 12, The Windows branch currently lacks any export
configuration when target.as_str() == "windows" and capi_enabled is true; update
the build.rs match to handle that case by either (a) generating a
module-definition (.def) file listing the C-API symbols and emitting a
cargo:rustc-link-arg-bin=rustpython=/DEF:<path> (or rustc-link-arg for the MSVC
linker) or (b) detecting MinGW and emitting the appropriate
-Wl,--export-all-symbols flag; implement one of these in the "windows" arm and
add a brief comment that documents which method you chose and why (reference the
target.as_str() match, the capi_enabled flag, and the println! calls that emit
cargo:rustc-link-arg-bin).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/capi/.cargo/config.toml`:
- Around line 1-3: The PYO3 env entries (PYO3_CONFIG_FILE and PYO3_NO_PYTHON)
defined in the local crate .cargo/config.toml won't be picked up for
workspace-root builds; move those two entries into the workspace root
.cargo/config.toml, keep PYO3_CONFIG_FILE pointed at pyo3-rustpython.config (do
not change it to ../pyo3-rustpython.config since relative=true already resolves
to the crate dir), and remove the duplicate/local config so workspace builds
(e.g., builds with the capi feature) inherit the PYO3 settings.

In `@crates/capi/src/object.rs`:
- Around line 32-67: PyType_GetFlags currently initializes flags from
ty.slots.flags.bits() (u64) and then ORs c_ulong constants
(PY_TPFLAGS_LONG_SUBCLASS, etc.), which breaks on Windows because c_ulong is
u32; fix by casting the initial value to c_ulong—i.e., change the initialization
of flags in PyType_GetFlags so it uses ty.slots.flags.bits() as c_ulong before
any |= with the PY_TPFLAGS_* constants so all operations are the same c_ulong
type.

In `@crates/capi/src/pyerrors.rs`:
- Around line 99-105: PyErr_Occurred currently returns the exception instance
(exc.as_object().as_raw()) but must return the exception type (the first arg to
PyErr_Set*), so change the mapper in PyErr_Occurred to return the exception's
type raw pointer instead — i.e., when vm.current_exception() yields exc, return
the raw pointer for exc's class/type (use the VM/exception API such as
exc.class().as_object().as_raw() or the equivalent on your exception type) and
keep the borrowed semantics (no INCREF), preserving the unwrap_or_default
fallback.
- Around line 118-123: In PyErr_SetRaisedException, validate the incoming
pointer before taking ownership: check that exc is not null (use NonNull::new
instead of NonNull::new_unchecked and return an appropriate Err if null), then
construct the PyObjectRef via PyObjectRef::from_raw only after the non-null
check, and replace downcast_unchecked() with a checked downcast (e.g.,
downcast() or the crate's checked variant) so you verify the object is an
exception instance before returning Err(exception).
- Around line 192-236: The extern "C" functions (notably PyErr_NewException,
PyErr_NewExceptionWithDoc, PyErr_PrintEx, PyErr_DisplayException,
PyErr_WriteUnraisable) currently call expect!/panic!/assert!, which will abort
the process on bad input; instead catch invalid inputs, set a Python SystemError
on the VM and return NULL (or return early) per the C-API contract. Replace uses
of expect()/panic!/assert! in PyErr_NewException (e.g.,
CStr::from_ptr(...).to_str().expect(...), rsplit_once().expect(...), panic! for
bad base types, and assert! on dict) with guarded checks that build an error
message, call vm.ctx.exceptions.system_error (or
vm.set_exception(vm.ctx.exceptions.system_error, message)) to set the exception
on the VM, and then return std::ptr::null_mut(); apply the same pattern to
PyErr_NewExceptionWithDoc (delegate should propagate NULL on error) and to
PyErr_PrintEx / PyErr_DisplayException / PyErr_WriteUnraisable so they no longer
panic but set an appropriate SystemError and return gracefully.

In `@crates/capi/src/pylifecycle.rs`:
- Around line 42-53: Py_FinalizeEx() currently returns success without resetting
interpreter state; update Py_FinalizeEx to properly finalize and clear
MAIN_INTERP (drop/free the interpreter handle and set MAIN_INTERP to null/None)
and update whatever initialized flag Py_IsInitialized() checks so it returns 0
after finalize; ensure Py_Finalize() still calls Py_FinalizeEx(), and mirror the
same teardown in Py_FinalizeEx so a subsequent Py_InitializeEx() can create a
fresh interpreter. Reference: Py_FinalizeEx, Py_Finalize, MAIN_INTERP,
Py_IsInitialized, Py_InitializeEx.

In `@crates/capi/src/pystate.rs`:
- Around line 46-49: The PyEval_SaveThread stub must not silently return NULL;
update the body of the extern "C" function PyEval_SaveThread to fail loudly
instead of ptr::null_mut(): add a TODO comment about implementing proper GIL
release, and replace the current return with either an explicit abort (print a
clear message then process::abort()) or call unimplemented!() so callers (and C
extensions expecting a valid PyThreadState pointer) will fail loudly during
development; also ensure the complementary PyEval_RestoreThread handling is
noted in the TODO so the full pair gets implemented against
release_current_thread/attach_current_thread later.

In `@crates/vm/src/codecs.rs`:
- Around line 161-164: The cfg gates are inconsistent:
CodecsRegistry::reinit_after_fork and StringPool::reinit_after_fork are only
defined with #[cfg(all(unix, feature = "threading", feature = "host_env"))] but
posix::reinit_locks_after_fork (posix.rs::reinit_locks_after_fork) is gated with
#[cfg(all(unix, feature = "threading"))], causing missing method errors when
host_env is disabled; update the cfg on posix::reinit_locks_after_fork to
#[cfg(all(unix, feature = "threading", feature = "host_env"))] so its calls to
CodecsRegistry::reinit_after_fork and StringPool::reinit_after_fork compile
consistently.

In `@crates/vm/src/intern.rs`:
- Around line 39-42: The function reinit_locks_after_fork is currently gated
with #[cfg(all(unix, feature = "threading"))] but calls
StringPool::reinit_after_fork and CodecRegistry::reinit_after_fork which are
defined only when #[cfg(all(unix, feature = "threading", feature = "host_env"))]
is set; change the attribute on reinit_locks_after_fork to #[cfg(all(unix,
feature = "threading", feature = "host_env"))] so the compilation gate matches
the methods it calls (locate the reinit_locks_after_fork function and update its
cfg to include feature = "host_env").

In `@crates/vm/src/stdlib/_io.rs`:
- Around line 5-6: The exported function cfg is too restrictive: change the
module-level export so reinit_std_streams_after_fork and reinit_io_locks are
available under #[cfg(all(unix, feature = "threading"))] and move the
#[cfg(feature = "host_env")] guard inside each function body; implement the
host_env-disabled path as a no-op (early return) so calls from posix.rs (which
are under #[cfg(feature = "threading")]) compile when host_env is not enabled,
while keeping the real implementation when host_env is present.

In `@crates/vm/src/stdlib/_thread.rs`:
- Around line 2-3: The call site py_os_after_fork_child in posix.rs calls
after_fork_child which is only exported under #[cfg(all(unix, feature =
"threading", feature = "host_env"))]; update the gating around
py_os_after_fork_child (or the specific call) to require the same cfg (e.g.
#[cfg(all(unix, feature = "threading", feature = "host_env"))]) so the call is
only compiled when after_fork_child is available, ensuring the cfg for
py_os_after_fork_child matches the after_fork_child export.

In `@crates/vm/src/vm/thread.rs`:
- Around line 175-190: The release_current_thread function currently drops
GILSTATE_VM before popping the raw pointer from VM_STACK, which can cause
destructors to call with_current_vm and dereference freed memory; fix by
reversing the order in release_current_thread: first remove/pop the VM pointer
from VM_STACK (the vms.borrow_mut().pop().expect(...) call) and only after that
take/drop the GILSTATE_VM (GILSTATE_VM.with(|gilstate_vm|
gilstate_vm.borrow_mut().take())); keep the existing detach_thread call and
other logic unchanged.

---

Outside diff comments:
In `@crates/vm/src/object/core.rs`:
- Around line 477-503: The call site gating for reset_weakref_locks_after_fork
in posix.rs::py_os_after_fork_child must match the function's cfg; change the
cfg on the call to require both feature = "threading" and feature = "host_env"
(i.e. use #[cfg(all(feature = "threading", feature = "host_env"))]) so the call
to crate::object::reset_weakref_locks_after_fork() is only compiled when the
function is present.

In `@crates/vm/src/vm/mod.rs`:
- Around line 2063-2098: The C-API error-indicator must be backed by a separate
per-Vm state, not the exc_info stack used by sys.exc_info()/PUSH_EXC_INFO;
change the methods so they don't read/write exceptions.stack: leave
current_exception/set_exception (and their semantics for the exc_info stack)
as-is, and introduce/replace the C-API facing API with a separate storage (e.g.,
add a new field like raised_exception: RefCell<Option<PyBaseExceptionRef>>) and
implement get_raised_exception / set_raised_exception / take_raised_exception to
operate on that field instead of exceptions.stack; ensure
thread::update_thread_exception continues to reflect the thread-visible error
indicator using the new getter (not topmost_exception), and update any callers
that relied on the old take_raised_exception to use the new C-API methods so
C-level errors do not corrupt the handled-exception stack.

---

Nitpick comments:
In `@build.rs`:
- Around line 5-12: The Windows branch currently lacks any export configuration
when target.as_str() == "windows" and capi_enabled is true; update the build.rs
match to handle that case by either (a) generating a module-definition (.def)
file listing the C-API symbols and emitting a
cargo:rustc-link-arg-bin=rustpython=/DEF:<path> (or rustc-link-arg for the MSVC
linker) or (b) detecting MinGW and emitting the appropriate
-Wl,--export-all-symbols flag; implement one of these in the "windows" arm and
add a brief comment that documents which method you chose and why (reference the
target.as_str() match, the capi_enabled flag, and the println! calls that emit
cargo:rustc-link-arg-bin).

In `@crates/capi/src/lib.rs`:
- Around line 27-33: The code currently calls
init_exception_statics(&Context::genesis().exceptions) which may diverge if
Context becomes per-interpreter; change set_main_interpreter to derive
exceptions from the Interpreter being installed by entering the interpreter
(e.g., use Interpreter::enter or interpreter.enter(|vm| ...)) and call
init_exception_statics(&vm.ctx.exceptions) while still in that closure, then
store the interpreter; ensure the safety comment is updated to reflect that
exception statics are initialized from the interpreter's Context and keep the
assert!(interp.is_none()) and assignment to *interp = Some(interpreter) as
before.

In `@crates/capi/src/object.rs`:
- Around line 70-83: In Py_GetConstantBorrowed replace the panic! on the _
(invalid id) branch with CPython-style behavior: create/raise a SystemError on
the VM/context (e.g. via vm.ctx.new_system_error(...) or the crate's VM
exception-raising helper) and return null_mut() from the extern "C" function;
alternatively, change the function to return an FfiResult that sets the
exception and yields a null pointer on error. Ensure you still return the
borrowed object's as_raw() for valid ids and do not unwind across the FFI
boundary.

In `@crates/capi/src/pystate.rs`:
- Around line 11-13: with_vm currently calls with_current_vm which holds a
RefCell borrow on VM_STACK for the duration of the callback; if the provided
closure f (or anything it transitively calls) tries to mutate VM_STACK (for
example by calling release_current_thread) it will cause a borrow_mut panic —
add a concise safety comment on with_vm explaining that the closure must not
call any function that mutates or re-enters VM_STACK (e.g.,
release_current_thread or other VM stack-mutating APIs) and suggest using a
different API or refactoring to avoid holding the borrow across such calls;
reference the functions with_vm and with_current_vm and the VM_STACK cell in the
comment to help future callers locate the caveat.

In `@crates/capi/src/util.rs`:
- Around line 79-85: The impl FfiResult<isize> for usize currently casts usize
-> isize in into_output which can wrap for values > isize::MAX and collide with
ERR_VALUE; change into_output in the impl for usize to convert safely (e.g., use
isize::try_from(self).unwrap_or(isize::MAX) or otherwise saturate/validate and
return isize::MAX on overflow) and document the chosen behavior, making sure
ERR_VALUE (-1) remains distinct; update the impl block (symbols:
FfiResult<isize> for usize, const ERR_VALUE, fn into_output(self, _vm:
&VirtualMachine)) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7a69f801-df68-4089-9da5-e5695cac77a4

📥 Commits

Reviewing files that changed from the base of the PR and between 22d4f43 and 61f4387.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .cspell.dict/cpython.txt
  • .cspell.dict/python-more.txt
  • Cargo.toml
  • build.rs
  • crates/capi/.cargo/config.toml
  • crates/capi/Cargo.toml
  • crates/capi/pyo3-rustpython.config
  • crates/capi/src/lib.rs
  • crates/capi/src/object.rs
  • crates/capi/src/pyerrors.rs
  • crates/capi/src/pylifecycle.rs
  • crates/capi/src/pystate.rs
  • crates/capi/src/refcount.rs
  • crates/capi/src/util.rs
  • crates/vm/Cargo.toml
  • crates/vm/src/builtins/type.rs
  • crates/vm/src/codecs.rs
  • crates/vm/src/intern.rs
  • crates/vm/src/object/core.rs
  • crates/vm/src/stdlib/_imp.rs
  • crates/vm/src/stdlib/_io.rs
  • crates/vm/src/stdlib/_thread.rs
  • crates/vm/src/vm/mod.rs
  • crates/vm/src/vm/thread.rs
  • src/lib.rs
💤 Files with no reviewable changes (1)
  • crates/vm/Cargo.toml

2 hidden conversations Load more…
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.

2 participants