From b0f0baad53b67a8b66288d95af531d9a967850da Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 18 Jul 2026 17:20:34 +0900 Subject: [PATCH 1/4] ctypes: preserve result when errcheck returns args --- crates/vm/src/stdlib/_ctypes/function.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index ebda717192a..cf655191683 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -1554,7 +1554,11 @@ fn build_result( let args_tuple = PyTuple::new_ref(args.args.clone(), &vm.ctx); let func_obj = zelf.as_object().to_owned(); let result_obj = result.clone().unwrap_or_else(|| vm.ctx.none()); - result = Some(errcheck.call((result_obj, func_obj, args_tuple), vm)?); + let checked = errcheck.call((result_obj, func_obj, args_tuple.clone()), vm)?; + // Returning the original args tuple requests normal result processing. + if !checked.is(&args_tuple) { + result = Some(checked); + } } // Handle OUT parameter return values From 7eed565146dcf23c2aed27c0a6d77d8196bd5aa2 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 18 Jul 2026 23:42:36 +0900 Subject: [PATCH 2/4] ctypes: cover Windows pip truststore --- .github/workflows/ci.yaml | 12 ++++++++++- crates/vm/src/stdlib/_ctypes/array.rs | 16 ++++++++++++++ extra_tests/snippets/stdlib_ctypes.py | 31 +++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1125c178bec..d03735192c8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -438,6 +438,17 @@ jobs: target/release/rustpython -m ensurepip target/release/rustpython -c "import pip" + - if: runner.os == 'Windows' + name: Check pip HTTPS with the Windows trust store + run: >- + target/release/rustpython -m pip download + --disable-pip-version-check + --no-cache-dir + --no-deps + --only-binary=:all: + --dest "$env:RUNNER_TEMP\rustpython-pip-smoke" + six + - if: runner.os != 'Windows' name: Check if pip inside venv is functional run: | @@ -829,4 +840,3 @@ jobs: - name: cargo doc run: cargo doc --locked - diff --git a/crates/vm/src/stdlib/_ctypes/array.rs b/crates/vm/src/stdlib/_ctypes/array.rs index f7abc834564..a99fabc812d 100644 --- a/crates/vm/src/stdlib/_ctypes/array.rs +++ b/crates/vm/src/stdlib/_ctypes/array.rs @@ -641,6 +641,14 @@ impl PyCArray { let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm); zelf.0.keep_alive(index, kept_alive); (ptr, Some(value.to_owned())) + } else if let Some(simple) = value.downcast_ref::() + && value.class().type_code(vm).as_deref() == Some("z") + { + let buffer = simple.0.buffer.read(); + ( + rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer), + None, + ) } else if let Ok(int_val) = value.try_index(vm) { (int_val.as_bigint().to_usize().unwrap_or(0), None) } else { @@ -667,6 +675,14 @@ impl PyCArray { } else if let Some(s) = value.downcast_ref::() { let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm); (ptr, Some(holder)) + } else if let Some(simple) = value.downcast_ref::() + && value.class().type_code(vm).as_deref() == Some("Z") + { + let buffer = simple.0.buffer.read(); + ( + rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer), + None, + ) } else if let Ok(int_val) = value.try_index(vm) { (int_val.as_bigint().to_usize().unwrap_or(0), None) } else { diff --git a/extra_tests/snippets/stdlib_ctypes.py b/extra_tests/snippets/stdlib_ctypes.py index 0a5d1387a8d..7cd3fcf2639 100644 --- a/extra_tests/snippets/stdlib_ctypes.py +++ b/extra_tests/snippets/stdlib_ctypes.py @@ -190,6 +190,10 @@ def __repr__(self): _check_size(c_char_p, "P") +char_pointer = c_char_p(b"1.3.6.1.5.5.7.3.1") +char_pointer_array = (c_char_p * 1)(char_pointer) +assert char_pointer_array[0] == b"1.3.6.1.5.5.7.3.1" + class c_void_p(_SimpleCData): _type_ = "P" @@ -344,7 +348,9 @@ def LoadLibrary(self, name): # print(libc.srand(i)) # print(test_byte_array) else: + import ctypes import os + from ctypes import wintypes libc = cdll.msvcrt libc.rand() @@ -356,6 +362,29 @@ def LoadLibrary(self, name): # print("start printf") # libc.printf(test_byte_array) + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + get_current_process = kernel32.GetCurrentProcess + get_current_process.argtypes = () + get_current_process.restype = ctypes.c_void_p + + def preserve_result(_result, _func, args): + return args + + get_current_process.errcheck = preserve_result + process_handle = get_current_process() + assert isinstance(process_handle, int) + + get_process_id = kernel32.GetProcessId + get_process_id.argtypes = (ctypes.c_void_p,) + get_process_id.restype = wintypes.DWORD + assert get_process_id(process_handle) == os.getpid() + + def replace_result(_result, _func, _args): + return "replacement" + + get_current_process.errcheck = replace_result + assert get_current_process() == "replacement" + # windows pip support def get_win_folder_via_ctypes(csidl_name: str) -> str: @@ -364,8 +393,6 @@ def get_win_folder_via_ctypes(csidl_name: str) -> str: # Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead. # https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid - import ctypes # noqa: PLC0415 - csidl_const = { "CSIDL_APPDATA": 26, "CSIDL_COMMON_APPDATA": 35, From 94ed9ed4512be80b1017c5a55d49f96ab74f506b Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 19 Jul 2026 13:02:36 +0900 Subject: [PATCH 3/4] capi: implement PyEval_SaveThread --- crates/capi/src/pystate.rs | 45 ++++++++++++++++++++------- crates/stdlib/src/overlapped.rs | 3 +- crates/vm/src/vm/thread.rs | 55 +++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 1a2f66de9f1..6bfb011308a 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -2,9 +2,9 @@ use crate::get_main_interpreter; use crate::pylifecycle::request_vm_from_interpreter; use crate::util::FfiResult; use core::ffi::c_int; -use core::ptr; use rustpython_vm::vm::thread::{ - CurrentVmAttachState, attach_current_thread, release_current_thread, with_current_vm, + CurrentVmAttachState, SavedThreadState, attach_current_thread, release_current_thread, + restore_current_thread, save_current_thread, with_current_vm, }; use rustpython_vm::{Interpreter, VirtualMachine}; @@ -24,6 +24,12 @@ pub struct PyThreadState { pub interp: *mut PyInterpreterState, } +#[repr(C)] +struct SavedPyThreadState { + public: PyThreadState, + vm: SavedThreadState, +} + /// Make sure this thread has a running vm attached. This only creates a new vm if we don't already /// have one. So this will only create a new vm when we are in a new thread created outside RustPython. pub(crate) fn ensure_thread_has_vm_attached() -> CurrentVmAttachState { @@ -47,11 +53,22 @@ pub extern "C" fn PyGILState_Release(state: PyGILState_STATE) { #[unsafe(no_mangle)] pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { - ptr::null_mut() + let interp = PyInterpreterState_Get(); + let state = Box::new(SavedPyThreadState { + public: PyThreadState { interp }, + vm: save_current_thread(), + }); + Box::into_raw(state).cast() } #[unsafe(no_mangle)] -pub extern "C" fn PyEval_RestoreThread(_state: *mut PyThreadState) {} +pub unsafe extern "C" fn PyEval_RestoreThread(state: *mut PyThreadState) { + assert!(!state.is_null(), "PyEval_RestoreThread called with null"); + // SAFETY: PyEval_SaveThread returns this allocation and CPython's API + // requires callers to restore exactly that thread state once. + let state = unsafe { Box::from_raw(state.cast::()) }; + restore_current_thread(state.vm); +} #[unsafe(no_mangle)] pub extern "C" fn PyInterpreterState_Get() -> *mut PyInterpreterState { @@ -81,7 +98,7 @@ mod tests { #[test] fn new_thread() { - Python::attach(|_py| { + Python::attach(|py| { with_current_vm(|_vm| { assert!( current_vm_is_set(), @@ -89,18 +106,24 @@ mod tests { ) }); - std::thread::spawn(move || { + let handle = std::thread::spawn(move || { Python::attach(|_py| { - with_current_vm(|_vm| { + with_current_vm(|vm| { assert!( current_vm_is_set(), "This thread did not have a vm attached" - ) + ); + vm.state.stop_the_world.stop_the_world(vm); + vm.state.stop_the_world.start_the_world(vm); }); }); - }) - .join() - .unwrap(); + }); + + py.detach(|| { + assert!(!current_vm_is_set()); + handle.join().unwrap(); + }); + assert!(current_vm_is_set()); }) } diff --git a/crates/stdlib/src/overlapped.rs b/crates/stdlib/src/overlapped.rs index 8610fadb3bf..86ac24e3a0f 100644 --- a/crates/stdlib/src/overlapped.rs +++ b/crates/stdlib/src/overlapped.rs @@ -1163,7 +1163,8 @@ mod _overlapped { #[pyfunction] fn GetQueuedCompletionStatus(port: isize, msecs: u32, vm: &VirtualMachine) -> PyResult { - match host_overlapped::get_queued_completion_status(port, msecs) + match vm + .allow_threads(|| host_overlapped::get_queued_completion_status(port, msecs)) .map_err(|err| set_from_windows_err(err.raw_os_error().unwrap_or(0) as u32, vm))? { host_overlapped::WaitResult::Timeout => Ok(vm.ctx.none()), diff --git a/crates/vm/src/vm/thread.rs b/crates/vm/src/vm/thread.rs index 5009cb695c6..9948b936f9d 100644 --- a/crates/vm/src/vm/thread.rs +++ b/crates/vm/src/vm/thread.rs @@ -224,6 +224,61 @@ pub enum CurrentVmAttachState { Attached, } +/// State preserved while the current native thread is detached from its VM. +#[cfg(feature = "threading")] +pub struct SavedThreadState { + vm_stack: Vec>, + gilstate_vm: Option>, +} + +/// Detach the current native thread and preserve its VM context for restoration. +#[cfg(feature = "threading")] +#[must_use = "the saved thread state must be restored"] +pub fn save_current_thread() -> SavedThreadState { + let vm_stack = VM_STACK.with(|vms| core::mem::take(&mut *vms.borrow_mut())); + assert!( + !vm_stack.is_empty(), + "save_current_thread() called without an attached VM" + ); + let gilstate_vm = GILSTATE_VM.with(|gilstate_vm| gilstate_vm.borrow_mut().take()); + detach_thread(); + SavedThreadState { + vm_stack, + gilstate_vm, + } +} + +/// Restore a VM context previously returned by [`save_current_thread`]. +#[cfg(feature = "threading")] +pub fn restore_current_thread(state: SavedThreadState) { + assert!( + !current_vm_is_set(), + "restore_current_thread() called with an attached VM" + ); + let SavedThreadState { + vm_stack, + gilstate_vm, + } = state; + let vm = vm_stack + .last() + .copied() + .expect("saved thread state has no VM"); + + GILSTATE_VM.with(|current| { + let mut current = current.borrow_mut(); + assert!( + current.is_none(), + "restore_current_thread() called with a GILState VM" + ); + *current = gilstate_vm; + }); + + // SAFETY: borrowed VMs remain alive for the dynamic save/restore scope, + // while an owned GILState VM was restored above before this dereference. + attach_thread(unsafe { vm.as_ref() }); + VM_STACK.with(|vms| *vms.borrow_mut() = vm_stack); +} + /// Attach the current native thread to a RustPython VM until /// `release_current_thread()` is called. #[cfg(feature = "threading")] From c200885bad48b7c7dacb03ed47faa9462f7c9576 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 20 Jul 2026 13:12:00 +0900 Subject: [PATCH 4/4] capi: merge saved thread state into PyThreadState --- crates/capi/src/pystate.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/capi/src/pystate.rs b/crates/capi/src/pystate.rs index 6bfb011308a..173c26088cf 100644 --- a/crates/capi/src/pystate.rs +++ b/crates/capi/src/pystate.rs @@ -22,11 +22,6 @@ pub type PyInterpreterState = Interpreter; #[repr(C)] pub struct PyThreadState { pub interp: *mut PyInterpreterState, -} - -#[repr(C)] -struct SavedPyThreadState { - public: PyThreadState, vm: SavedThreadState, } @@ -54,11 +49,11 @@ pub extern "C" fn PyGILState_Release(state: PyGILState_STATE) { #[unsafe(no_mangle)] pub extern "C" fn PyEval_SaveThread() -> *mut PyThreadState { let interp = PyInterpreterState_Get(); - let state = Box::new(SavedPyThreadState { - public: PyThreadState { interp }, + let state = Box::new(PyThreadState { + interp, vm: save_current_thread(), }); - Box::into_raw(state).cast() + Box::into_raw(state) } #[unsafe(no_mangle)] @@ -66,7 +61,7 @@ pub unsafe extern "C" fn PyEval_RestoreThread(state: *mut PyThreadState) { assert!(!state.is_null(), "PyEval_RestoreThread called with null"); // SAFETY: PyEval_SaveThread returns this allocation and CPython's API // requires callers to restore exactly that thread state once. - let state = unsafe { Box::from_raw(state.cast::()) }; + let state = unsafe { Box::from_raw(state) }; restore_current_thread(state.vm); }