Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Dec 19, 2025

Summary by CodeRabbit

New Features

  • Enhanced ctypes function pointer support with improved parameter and return value handling
  • Added generic parameterization support for ctypes arrays
  • Extended buffer protocol support for ctypes pointers and arrays

Bug Fixes

  • Improved error handling and reporting for dynamic library operations
  • Enhanced errno and last-error tracking on Windows
  • Better memory access handling for ctypes operations on strict-alignment architectures

Chores

  • Updated compiler information in version output for Windows

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 19, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Major refactoring of the ctypes module introducing new FFI calling machinery (Argument/OutBuffers abstractions), buffer protocol support across pointer/array types, enhanced error handling for errno/last-error, library loading improvements, field descriptor removal, and type system restructuring with platform-specific behaviors for Unix/Windows.

Changes

Cohort / File(s) Summary
Core FFI Calling Machinery
crates/vm/src/stdlib/ctypes/function.rs
Reworked FFI call pipeline from separate ffi_type/value pairs to unified Argument abstraction with OutBuffers for IN/OUT parameter handling. Enhanced result conversion with staged handling for pointers, structures, and scalars. Added PyCFuncPtrType metaclass and improved flags mechanism. Extended thunk creation to support errno handling and better error reporting.
Errno and Last-Error Handling
crates/vm/src/stdlib/ctypes.rs
Enhanced errno tracking returning old values; added Windows thread-local CTYPES_LOCAL_LAST_ERROR storage and save_and_restore_last_error wrapper. Changed call_function return type from isize to i32. Added dlclose/dlsym Unix helpers with error handling. Standardized SIZEOF_TIME_T to libc::time_t size. Initialized PyCFuncPtrType in module.
Library Loading and Management
crates/vm/src/stdlib/ctypes/library.rs
Widened SharedLibrary visibility to pub(crate). Added platform-specific constructors: new(name) for Windows and new_with_mode(name, mode)/from_raw_handle(handle) for Unix. Introduced get_pointer() returning OS handle via transmute_copy. Added Drop impl. Extended ExternalLibs with get_or_insert_lib (Windows) and get_or_insert_lib_with_mode (Unix). Added insert_raw_handle for Unix.
Pointer Buffer Protocol
crates/vm/src/stdlib/ctypes/pointer.rs
Implemented AsBuffer for PyCPointer and PyCPointerType enabling buffer protocol access. Extended array shape encoding in storage format strings. Added CArgObject (byref) support in from_param. Reworked memory I/O to use read_unaligned/write_unaligned for multi-byte types. Enhanced contents handling with KeepRef semantics.
Array and Simple Type Buffer Support
crates/vm/src/stdlib/ctypes/array.rs
Added \class_getitem to PyCArray and PyCArrayType for generic parameterization returning PyGenericAlias. Introduced dynamic itemsize computation for empty/zero-dimension shapes using format string derivation. Updated BufferDescriptor and stride logic.
Simple Type Formatting and Buffers
crates/vm/src/stdlib/ctypes/simple.rs
Added ctypes_code_to_pep3118 and alloc_format_string_for_type for platform-aware PEP 3118 format mapping with endianness handling. Extended wchar/string pointer handling for Windows (u16) and Unix (i32). Revised AsBuffer for PyCSimple deriving format/itemsize from StgInfo. Enhanced wide-string reading with surrogate/UTF-32 handling. Changed is_cwchar_array_or_pointer to return PyResult.
Base Type Format and Field Resolution
crates/vm/src/stdlib/ctypes/base.rs
Enhanced get_field_format to handle complex ctypes formats (T, (, &, X{) as-is while applying endian prefixes to simple types. Relaxed \type attribute condition from single-char to non-empty strings. Refactored in_dll to use cache-backed library symbol lookup instead of platform-specific calls. Changed PyCField::descr_set and FfiArgValue::OwnedPointer type signatures. Added is_simple_instance helper.
Field Descriptor Removal
crates/vm/src/stdlib/ctypes/field.rs
Deleted entire PyCFieldType and PyCField implementations including descriptor logic, byte conversions (bytes_to_value/value_to_bytes), field metadata accessors, and \set/\delete bindings. Removes public Field/CField API surface.
Structure Updates
crates/vm/src/stdlib/ctypes/structure.rs
Removed public \fields getter on PyCStructure instance level, affecting API surface for Structure instance access.
Version String Formatting
crates/vm/src/version.rs
Added platform-conditional compiler metadata: Windows branch generates "[RustPython {version} MSC v.1929 {arch}]" format; non-Windows uses RUSTC_VERSION environment variable for "[RustPython {version} with {compiler}]" format.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • FFI calling pipeline refactoring (function.rs): The switch from separate type/value collections to Argument abstraction affects call construction, argument conversion, result handling, and thunk integration—requires careful tracing of data flow through multiple paths
  • Library loading platform-specific changes (library.rs, ctypes.rs): New constructor signatures and get_or_insert_lib variants for Unix/Windows need validation for error handling consistency and handle management correctness
  • Buffer protocol implementations (pointer.rs, array.rs, simple.rs): New AsBuffer trait implementations need verification that format strings, itemsizes, and buffer descriptors match Python buffer protocol expectations
  • Errno/last-error Windows handling (ctypes.rs): Thread-local storage for last-error and save_and_restore_last_error wrapper interactions with thunk callbacks need careful review for correctness
  • Field descriptor removal and base type changes (field.rs, base.rs): Deletion of PyCField public API and refactored symbol resolution via cache impacts downstream code paths—verify in_dll callers adapt correctly
  • Type signature adjustments (base.rs): Changes to PyCField::descr_set parameter and FfiArgValue variant types should be verified against all call sites

Possibly related PRs

  • ctypes overhaul #6450: Extensive overlapping changes to same ctypes files (function, library, pointer, simple, array, structure, thunk) introducing/altering identical types and FFI/library-loading logic
  • ctypes buffer #6311: Substantial overlapping modifications to ctypes call/thunk machinery (function.rs, thunk.rs), library handling, and type metadata (StgInfo, PyCData)
  • Fix import ctypes #6304: Related Unix library-loading behavior changes (dlopen(NULL), load_library_unix signature) and current-process handle semantics

Suggested reviewers

  • arihant2math
  • coolreader18

Poem

🐰 A rabbit hops through ctypes lanes,
With Arguments and Buffers reigned,
Errno safely tucked in thread-local care,
Field descriptors cleared with flair,
Now pointers dance the buffer way! 🎉

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Enable ctypes' directly summarizes the main objective of the pull request, which is to enable and implement comprehensive ctypes support across multiple modules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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.

@github-actions
Copy link
Contributor

Code has been automatically formatted

The code in this PR has been formatted using cargo fmt --all.
Please pull the latest changes before pushing again:

git pull origin ctypes

@youknowone youknowone force-pushed the ctypes branch 8 times, most recently from bb11608 to d28fddf Compare December 20, 2025 16:13
@youknowone youknowone marked this pull request as ready for review December 21, 2025 00:41
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (4)
crates/vm/src/stdlib/ctypes/pointer.rs (1)

687-777: Integer writes via write_value_at_address can panic on overflow; return OverflowError instead.

The branches for integer sizes use expect("int too large"):

let i = int_val.as_bigint();
match size {
    1 => *ptr = i.to_u8().expect("int too large");
    2 => write_unaligned(ptr as *mut i16, i.to_i16().expect("int too large"));
    4 => write_unaligned(ptr as *mut i32, i.to_i32().expect("int too large"));
    8 => write_unaligned(ptr as *mut i64, i.to_i64().expect("int too large"));}

For a large ctypes.c_int assigned through a pointer (e.g. ptr[0] = 1 << 1000), this will panic the Rust VM instead of raising an OverflowError like CPython.

Recommend converting these to checked conversions that map failure to a Python exception, e.g.:

-    if let Ok(int_val) = value.try_int(vm) {
-        let i = int_val.as_bigint();
-        match size {
-            1 => {
-                *ptr = i.to_u8().expect("int too large");
-            }
+    if let Ok(int_val) = value.try_int(vm) {
+        let i = int_val.as_bigint();
+        match size {
+            1 => {
+                let v = i.to_u8().ok_or_else(|| vm.new_overflow_error("int too large for 1‑byte field"))?;
+                *ptr = v;
+            }
             2 => {
-                std::ptr::write_unaligned(ptr as *mut i16, i.to_i16().expect("int too large"));
+                let v = i.to_i16().ok_or_else(|| vm.new_overflow_error("int too large for 2‑byte field"))?;
+                std::ptr::write_unaligned(ptr as *mut i16, v);
             }
             4 => {
-                std::ptr::write_unaligned(ptr as *mut i32, i.to_i32().expect("int too large"));
+                let v = i.to_i32().ok_or_else(|| vm.new_overflow_error("int too large for 4‑byte field"))?;
+                std::ptr::write_unaligned(ptr as *mut i32, v);
             }
             8 => {
-                std::ptr::write_unaligned(ptr as *mut i64, i.to_i64().expect("int too large"));
+                let v = i.to_i64().ok_or_else(|| vm.new_overflow_error("int too large for 8‑byte field"))?;
+                std::ptr::write_unaligned(ptr as *mut i64, v);
             }

Same concern applies to the P/z/Z pointer cases using to_usize().expect(...). This is user‑controlled input, so avoiding VM panics is important.

crates/vm/src/stdlib/ctypes/simple.rs (1)

722-976: Large integer conversions in value_to_bytes_endian can panic instead of raising OverflowError.

For many ctypes integer and pointer codes, you now do:

let v = int_val.as_bigint().to_i128().expect("int too large") as i16;
// or
let v = int_val.as_bigint().to_usize().expect("int too large for pointer");

If a user passes a value outside the target type’s range (e.g. c_short(1 << 1000) or c_void_p(1 << 200)), this will panic the Rust VM instead of raising a Python OverflowError as CPython does.

Because value_to_bytes_endian() is used from PyCSimple::slot_new, set_value, and from_param, this is a user‑reachable crash.

To fix this, consider:

  • Changing value_to_bytes_endian to return PyResult<Vec<u8>> so you can propagate OverflowError from here; or
  • Pre‑checking ranges in callers before calling into this helper.

Pattern sketch for one case:

-        "h" => {
-            if let Ok(int_val) = value.try_index(vm) {
-                let v = int_val.as_bigint().to_i128().expect("int too large") as i16;
-                return to_bytes!(v);
-            }
+        "h" => {
+            if let Ok(int_val) = value.try_index(vm) {
+                if let Some(v) = int_val.as_bigint().to_i16() {
+                    return to_bytes!(v);
+                }
+                // Caller can turn this into vm.new_overflow_error("...") if you propagate Result
+            }
             vec![0; 2]
        }

Similar guarded conversions are needed for all the to_i128().expect(...) / to_usize().expect(...) paths (integer and pointer types). Right now this is a correctness and robustness issue that should be addressed before relying on these paths heavily.

crates/vm/src/stdlib/ctypes.rs (1)

574-616: dlopen now returns a cache ID, but dlclose/dlsym call libc with that ID as a pointer (likely UB).

On Unix:

  • dlopen(...) uses library::libcache().get_or_insert_lib_with_mode / insert_raw_handle and returns a usize handle that is an index/key into ExternalLibs, not necessarily the raw *mut c_void from libc::dlopen.
  • free_library(handle) correctly passes that ID to drop_lib(handle).

However, the new functions:

fn dlclose(handle: usize, vm: &VirtualMachine) -> PyResult<()> {
    let result = unsafe { libc::dlclose(handle as *mut libc::c_void) };}

fn dlsym(handle: usize, name: PyStrRef, vm: &VirtualMachine) -> PyResult<usize> {
    let ptr = unsafe { libc::dlsym(handle as *mut libc::c_void, symbol_name.as_ptr()) };}

treat the same usize as a raw void* and pass it straight to dlclose/dlsym. If Python code calls _ctypes.dlopen and then _ctypes.dlsym / _ctypes.dlclose with that returned value (which matches CPython’s API shape), you’ll end up calling libc with an invalid pointer, which is undefined behavior and may segfault.

You probably want one of:

  • Make dlopen return the raw *mut c_void and keep the cache keyed separately, or
  • Keep returning the cache ID but have dlclose/dlsym look up the real OS handle via libcache().get_lib(id) and then call libc::dlclose/dlsym on that underlying handle (or delegate to ExternalLibs methods and avoid calling libc here entirely).

As it stands, these APIs are inconsistent and unsafe if used together.

Also applies to: 626-665

crates/vm/src/stdlib/ctypes/function.rs (1)

1946-1976: Incomplete errno swapping implementation.

The errno save/restore logic has TODO comments indicating incomplete implementation. The current code:

  1. Saves the current errno before the call
  2. Restores it after the call

But it doesn't implement the full CPython behavior which requires:

  1. Swap the OS errno with the ctypes thread-local stored errno before the call
  2. After the call, save the new OS errno to ctypes storage and restore the previous OS errno

This means ctypes.get_errno() and ctypes.set_errno() won't work correctly with callbacks.

Would you like me to help design the thread-local errno storage mechanism, or should this be tracked as a follow-up issue?

🧹 Nitpick comments (5)
crates/vm/src/stdlib/ctypes/array.rs (1)

17-28: PEP 3118 itemsize derivation looks correct; consider minor robustness tweaks.

The new get_size_from_format() + AsBuffer logic correctly recovers itemsize from the format string for empty/zero-dim arrays and uses shape to compute strides, matching CPython’s “element-size, not 0” behavior for empty arrays and zero strides when any dim is 0.

Two small robustness points you may want to consider:

  • shape.iter().product() on usize will wrap on overflow. Given shape ultimately comes from validated StgInfo, it’s probably fine in practice, but if you expect user‑supplied huge dimensions, a checked product with a graceful OverflowError would be safer.
  • get_size_from_format() defaults itemsize to 1 if type_info returns None. With the current type_info coverage this shouldn’t trigger, but if new complex format codes are introduced later, it might be safer to pass a default (e.g. stg_info.element_size) instead of silently using 1.

Functionally this is fine to merge; the above are defensive improvements you can apply later if needed.

Also applies to: 1081-1112

crates/vm/src/stdlib/ctypes/pointer.rs (1)

781-803: Pointer AsBuffer implementation is reasonable, minor format default nit.

Exposing _Pointer as a scalar buffer of length sizeof(void*) with the stored stg_info.format and ndim=0 is consistent with the rest of the ctypes buffer protocol; using CDATA_BUFFER_METHODS ensures writes go through the existing CData machinery.

The only minor nit is the default format of "&B" when no StgInfo.format is present; "&P" (pointer) might be a slightly more descriptive fallback, but this doesn’t affect correctness and can be adjusted later if desired.

crates/vm/src/stdlib/ctypes/library.rs (3)

73-77: Redundant Drop implementation.

The explicit close() call in Drop is unnecessary. When SharedLibrary is dropped, the PyMutex<Option<Library>> field is automatically dropped, which will drop the contained Library if present.

The current implementation adds lock contention overhead without benefit.

🔎 Consider removing the explicit Drop impl
-impl Drop for SharedLibrary {
-    fn drop(&mut self) {
-        self.close();
-    }
-}

94-115: Minor inefficiency: Library loaded before checking cache.

The library is loaded via SharedLibrary::new() before checking if an open copy already exists in the cache. If the library is already cached and open, the newly loaded handle is immediately dropped.

Consider checking the cache first to avoid unnecessary library loads.

🔎 Proposed optimization
 pub fn get_or_insert_lib(
     &mut self,
     library_path: impl AsRef<OsStr>,
     _vm: &VirtualMachine,
 ) -> Result<(usize, &SharedLibrary), libloading::Error> {
-    let new_lib = SharedLibrary::new(library_path)?;
-    let key = new_lib.get_pointer();
-
-    match self.libraries.get(&key) {
-        Some(l) => {
-            if l.is_closed() {
-                self.libraries.insert(key, new_lib);
-            }
-        }
-        _ => {
-            self.libraries.insert(key, new_lib);
-        }
-    };
-
-    Ok((key, self.libraries.get(&key).expect("just inserted")))
+    let new_lib = SharedLibrary::new(&library_path)?;
+    let key = new_lib.get_pointer();
+
+    if let Some(existing) = self.libraries.get(&key) {
+        if !existing.is_closed() {
+            // Return existing open library, drop new_lib
+            return Ok((key, existing));
+        }
+    }
+    self.libraries.insert(key, new_lib);
+    Ok((key, self.libraries.get(&key).expect("just inserted")))
 }

138-138: Inconsistent error handling: unwrap() vs expect().

Line 138 uses .unwrap() while the Windows equivalent (line 114) uses .expect("just inserted"). For consistency and better panic messages, prefer expect() with a descriptive message.

🔎 Proposed fix
-        Ok((key, self.libraries.get(&key).unwrap()))
+        Ok((key, self.libraries.get(&key).expect("just inserted")))
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b3bf55 and 71288e1.

⛔ Files ignored due to path filters (70)
  • Lib/ctypes/__init__.py is excluded by !Lib/**
  • Lib/ctypes/_endian.py is excluded by !Lib/**
  • Lib/ctypes/test/__init__.py is excluded by !Lib/**
  • Lib/ctypes/test/__main__.py is excluded by !Lib/**
  • Lib/ctypes/test/test_delattr.py is excluded by !Lib/**
  • Lib/ctypes/test/test_simplesubclasses.py is excluded by !Lib/**
  • Lib/ctypes/util.py is excluded by !Lib/**
  • Lib/test/test_ctypes.py is excluded by !Lib/**
  • Lib/test/test_ctypes/__init__.py is excluded by !Lib/**
  • Lib/test/test_ctypes/__main__.py is excluded by !Lib/**
  • Lib/test/test_ctypes/_support.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_aligned_structures.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_anon.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_array_in_pointer.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_arrays.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_as_parameter.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_bitfields.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_buffers.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_bytes.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_byteswap.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_c_simple_type_meta.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_callbacks.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_cast.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_cfuncs.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_checkretval.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_delattr.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_dlerror.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_errno.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_find.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_frombuffer.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_funcptr.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_functions.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_incomplete.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_init.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_internals.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_keeprefs.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_libc.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_loading.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_macholib.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_memfunctions.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_numbers.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_objects.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_parameters.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_pep3118.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_pickling.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_pointers.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_prototypes.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_python_api.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_random_things.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_refcounts.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_repr.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_returnfuncptrs.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_simplesubclasses.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_sizes.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_slicing.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_stringptr.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_strings.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_struct_fields.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_structures.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_unaligned_structures.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_unicode.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_unions.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_values.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_varsize_struct.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_win32.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_win32_com_foreign_func.py is excluded by !Lib/**
  • Lib/test/test_ctypes/test_wintypes.py is excluded by !Lib/**
  • Lib/test/test_exceptions.py is excluded by !Lib/**
  • Lib/test/test_os.py is excluded by !Lib/**
  • Lib/test/test_threading.py is excluded by !Lib/**
📒 Files selected for processing (10)
  • crates/vm/src/stdlib/ctypes.rs (6 hunks)
  • crates/vm/src/stdlib/ctypes/array.rs (3 hunks)
  • crates/vm/src/stdlib/ctypes/base.rs (6 hunks)
  • crates/vm/src/stdlib/ctypes/field.rs (0 hunks)
  • crates/vm/src/stdlib/ctypes/function.rs (26 hunks)
  • crates/vm/src/stdlib/ctypes/library.rs (4 hunks)
  • crates/vm/src/stdlib/ctypes/pointer.rs (9 hunks)
  • crates/vm/src/stdlib/ctypes/simple.rs (12 hunks)
  • crates/vm/src/stdlib/ctypes/structure.rs (0 hunks)
  • crates/vm/src/version.rs (2 hunks)
💤 Files with no reviewable changes (2)
  • crates/vm/src/stdlib/ctypes/structure.rs
  • crates/vm/src/stdlib/ctypes/field.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/version.rs
  • crates/vm/src/stdlib/ctypes/array.rs
  • crates/vm/src/stdlib/ctypes/library.rs
  • crates/vm/src/stdlib/ctypes/simple.rs
  • crates/vm/src/stdlib/ctypes/pointer.rs
  • crates/vm/src/stdlib/ctypes.rs
  • crates/vm/src/stdlib/ctypes/base.rs
  • crates/vm/src/stdlib/ctypes/function.rs
🧠 Learnings (4)
📚 Learning: 2025-11-29T12:17:28.606Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T12:17:28.606Z
Learning: Applies to Lib/**/*.py : Minimize modifications to CPython standard library files in the `Lib/` directory; modifications should be minimal and only to work around RustPython limitations

Applied to files:

  • crates/vm/src/stdlib/ctypes/library.rs
📚 Learning: 2025-11-29T12:17:28.606Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T12:17:28.606Z
Learning: Applies to **/*.rs : Use the macro system (`pyclass`, `pymodule`, `pyfunction`, etc.) when implementing Python functionality in Rust

Applied to files:

  • crates/vm/src/stdlib/ctypes.rs
  • crates/vm/src/stdlib/ctypes/function.rs
📚 Learning: 2025-06-27T14:47:28.810Z
Learnt from: moreal
Repo: RustPython/RustPython PR: 5847
File: vm/src/stdlib/stat.rs:547-567
Timestamp: 2025-06-27T14:47:28.810Z
Learning: In RustPython's stat module implementation, platform-specific constants like SF_SUPPORTED and SF_SYNTHETIC should be conditionally declared only for the platforms where they're available (e.g., macOS), following CPython's approach of optional declaration using #ifdef checks rather than providing fallback values for other platforms.

Applied to files:

  • crates/vm/src/stdlib/ctypes.rs
📚 Learning: 2025-06-27T14:47:28.810Z
Learnt from: moreal
Repo: RustPython/RustPython PR: 5847
File: vm/src/stdlib/stat.rs:547-567
Timestamp: 2025-06-27T14:47:28.810Z
Learning: In RustPython's stat module implementation, platform-specific constants like SF_SUPPORTED and SF_SYNTHETIC should be conditionally declared only for the platforms where they're available (e.g., macOS), following CPython's approach of optional declaration rather than providing fallback values for other platforms.

Applied to files:

  • crates/vm/src/stdlib/ctypes.rs
🧬 Code graph analysis (4)
crates/vm/src/stdlib/ctypes/array.rs (3)
crates/vm/src/stdlib/ctypes.rs (3)
  • type_info (144-238)
  • t (55-55)
  • cls (696-696)
crates/vm/src/builtins/list.rs (1)
  • __class_getitem__ (340-342)
crates/vm/src/builtins/genericalias.rs (2)
  • args (102-102)
  • from_args (101-108)
crates/vm/src/stdlib/ctypes/pointer.rs (5)
crates/vm/src/stdlib/ctypes/union.rs (2)
  • base (114-114)
  • as_buffer (554-577)
crates/vm/src/stdlib/ctypes/base.rs (28)
  • std (376-376)
  • std (377-377)
  • std (2149-2149)
  • std (2242-2242)
  • proto (291-293)
  • proto (928-928)
  • proto (993-993)
  • proto (1540-1540)
  • proto (1557-1557)
  • new (149-165)
  • new (1246-1254)
  • value (862-863)
  • value (871-871)
  • value (871-871)
  • value (893-893)
  • value (907-907)
  • value (922-922)
  • value (932-932)
  • value (1361-1361)
  • value (1369-1369)
  • value (1377-1377)
  • value (1426-1426)
  • value (1441-1441)
  • value (1455-1455)
  • value (1469-1469)
  • value (1494-1494)
  • size (680-682)
  • size (1671-1673)
crates/vm/src/stdlib/ctypes/function.rs (8)
  • std (54-54)
  • std (61-61)
  • std (73-73)
  • std (126-126)
  • new (2032-2099)
  • value (87-87)
  • f (898-898)
  • as_buffer (1656-1680)
crates/vm/src/stdlib/ctypes/simple.rs (2)
  • new (239-245)
  • as_buffer (1405-1426)
crates/vm/src/stdlib/ctypes/array.rs (1)
  • as_buffer (1070-1122)
crates/vm/src/stdlib/ctypes.rs (1)
crates/vm/src/stdlib/ctypes/library.rs (3)
  • libcache (155-160)
  • new (23-27)
  • new (84-88)
crates/vm/src/stdlib/ctypes/base.rs (2)
crates/vm/src/stdlib/ctypes.rs (1)
  • stg_info (39-42)
crates/vm/src/stdlib/ctypes/library.rs (1)
  • libcache (155-160)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (windows-latest)
🔇 Additional comments (33)
crates/vm/src/version.rs (4)

18-33: LGTM - good compatibility workaround with clear documentation.

The hardcoded MSC version for ctypes compatibility is well-documented. Minor consideration: on ARM64 Windows, target_pointer_width = "64" would still report "AMD64" rather than "ARM64". This is likely acceptable since the goal is CPython compatibility for find_msvcrt(), but worth noting if ARM Windows support becomes important.


35-40: LGTM!

The non-Windows branch correctly uses the COMPILER constant with consistent formatting.


42-47: LGTM!

The format string structure is clear, and the PyPy convention for the newline separator is documented.


58-59: LGTM!

The #[cfg(not(windows))] attribute correctly prevents unused constant warnings on Windows builds since the constant is only used in the non-Windows code path.

crates/vm/src/stdlib/ctypes/array.rs (1)

463-466: __class_getitem__ implementation is consistent with other generics.

Using PyGenericAlias::from_args(cls, args, vm) matches the pattern in list.__class_getitem__ and keeps Array typing behavior aligned with other builtins.

crates/vm/src/stdlib/ctypes/base.rs (2)

296-341: Endian-aware get_field_format logic matches ctypes expectations.

The updated get_field_format cleanly separates:

  • Complex formats (T{…}, (…)…, &…, X{…}) which are returned as-is, preserving their internal endianness/shape encoding.
  • Simple formats where you strip any existing endian prefix and re‑apply > / < based on the big_endian flag.
  • A fallback path based on _type_ when StgInfo.format is absent.

This is a sensible mirror of CPython’s behavior and should work well with the new StgInfo.format generation in simple.rs.


1182-1208: Library symbol resolution + helper tweaks look good.

  • The new in_dll path that goes via library::libcache().get_lib(handle) and then lib.get::<*const u8>(symbol) improves error reporting (“Library not found”, “symbol … not found in library”) and centralizes handle management.
  • FfiArgValue::OwnedPointer(usize, PyObjectRef) now uses the crate-wide alias, which simplifies typing and matches other code in this module.
  • The is_simple_instance() helper correctly uses the metaclass to detect PyCSimpleType subclasses and will be handy to keep logic in one place.

No functional issues spotted here.

Also applies to: 1816-1817, 2157-2165

crates/vm/src/stdlib/ctypes/simple.rs (5)

24-57: PEP 3118 format helpers and swapped-type setup are well factored.

  • ctypes_code_to_pep3118() centralizes mapping from ctypes codes (i, l, q, ?, etc.) to fixed-size PEP 3118 codes, which simplifies keeping StgInfo.format consistent across platforms.
  • alloc_format_string_for_type() + its use in PyCSimpleType::init and create_swapped_types() gives native and swapped types explicit </> prefixes without sprinkling format-string construction everywhere.
  • In create_swapped_types(), using is_little_endian and passing it as the big_endian flag to alloc_format_string_for_type() correctly generates the opposite endian prefix for the swapped type.

The overall design here is clean and should make buffer/FFI format handling much easier to reason about.

Also applies to: 568-577, 601-677


203-216: "X" (BSTR) handling in set_primitive matches "Z" semantics.

Treating "X" as “BSTR-like” with the same acceptance rules as "Z" (None, int address, or unicode string) is consistent with the rest of the pointer-like simple types and matches how COM BSTRs are typically exposed. No issues here.


1000-1019: is_cwchar_array_or_pointer change to PyResult<bool> is an improvement.

Switching to stg_info(vm)? and returning PyResult<bool> lets you propagate “abstract class” and other type errors instead of silently treating them as “not a wchar array/pointer”. The updated call site in from_param correctly uses ? to bubble up failures.


1166-1187: Improved c_wchar_p value decoding across platforms.

The new Z branch in PyCSimple::value:

  • Uses Wtf8Buf::from_wide on Windows to handle UTF‑16 surrogate pairs correctly.
  • Falls back to simple char::from_u32 for UTF‑32 wchar_t on non‑Windows platforms.

This is a significant improvement over treating all wchar_t as 32‑bit, and aligns with how Windows error messages and wide strings behave.


1404-1425: AsBuffer for PyCSimple is consistent with the new StgInfo.format.

Using:

  • stg_info.size as both len and itemsize, and
  • stg_info.format (with "B" as a fallback),

to build a scalar (ndim=0) BufferDescriptor is in line with the rest of the ctypes buffer implementations (arrays, unions, pointers). This should make memoryview(c_int()) etc. behave predictably under the new PEP 3118 format scheme.

crates/vm/src/stdlib/ctypes.rs (4)

90-111: Module init now registers PyCFuncPtrType correctly.

Adding function::PyCFuncPtrType::make_class(ctx); alongside the other ctypes metaclasses ensures the function pointer metatype is initialized before you export CFuncPtr and related helpers. This matches how the other ctypes types are wired into _ctypes.


389-391: SIZEOF_TIME_T based on libc::time_t is appropriate.

Using std::mem::size_of::<libc::time_t>() for SIZEOF_TIME_T tracks the actual C ABI on the host and is preferable to hardcoding per‑platform constants.


961-1008: Errno and Windows last-error helpers look correct and match ctypes expectations.

  • set_errno now returns the previous value while updating errno, which matches Python’s ctypes.set_errno.
  • On Windows, CTYPES_LOCAL_LAST_ERROR provides thread-local storage for _ctypes’ “last error”, and get_last_error / set_last_error operate solely on that TLS value.
  • save_and_restore_last_error correctly restores the thread-local value into the OS LastError before the call and saves the new LastError back into TLS afterwards.

This is the expected pattern for use_last_error=True semantics and should integrate cleanly with the updated FFI call paths.


1124-1173: Default FFI return type changed to c_int; behavior looks intentional.

Switching call_function_internal to:

let cif = Cif::new(arg_types, Type::c_int());
let result: i32 = unsafe { cif.call(code_ptr, &ffi_args) };
Ok(vm.ctx.new_int(result).into())

makes the default return type c_int (32‑bit) instead of isize, aligning with CPython’s _ctypes.call_function / _ctypes.call_cdeclfunction behavior. The argument marshalling is unchanged, so this looks like a deliberate compatibility improvement.

crates/vm/src/stdlib/ctypes/library.rs (4)

11-13: LGTM!

The visibility change to pub struct SharedLibrary with pub(crate) lib field provides appropriate encapsulation while allowing necessary access within the crate.


141-148: LGTM!

The insert_raw_handle method correctly uses the handle value as the cache key, consistent with get_pointer() behavior.


49-61: The transmute_copy approach is appropriate for this use case, but document the design decision clearly.

Using transmute_copy to extract the OS handle is the correct pattern here—libloading::Library::into_raw() consumes ownership and is incompatible with keeping the library alive in a Mutex. However, clarify in comments that this is intentional to avoid ownership transfer and that the library's internal layout (storing the handle pointer as the first field) is relied upon. Consider adding a version constraint on libloading in Cargo.toml if not already present to guard against breaking changes.


41-47: The code correctly handles dlopen(NULL) handles. When SharedLibrary is dropped, the underlying libloading::Library calls dlclose, which safely decrements the reference count on the main program handle without unloading it. This is standard behavior protected by reference counting and poses no risk.

Likely an incorrect or invalid review comment.

crates/vm/src/stdlib/ctypes/function.rs (13)

138-151: LGTM!

The integer-to-pointer conversion correctly handles negative values (allowing -1 as 0xFFFF...) and appropriately raises OverflowError for out-of-range values, matching CPython's PyLong_AsVoidPtr behavior.


196-212: LGTM!

The string-to-wide-string conversion correctly uses native byte order (to_ne_bytes()) which matches the platform's wchar_t representation. The keep object properly maintains buffer lifetime during the FFI call.


1161-1170: LGTM!

The Argument struct correctly bundles FFI type, value, and a keep object to maintain buffer lifetime during FFI calls. The #[allow(dead_code)] on keep is appropriate since the field's purpose is to prevent premature deallocation rather than direct access.


1358-1374: LGTM!

The ctypes_callproc function cleanly constructs the FFI call from the Argument slice, properly separating type and value extraction. The return type dispatch handles void, pointer, and scalar returns appropriately.


1619-1637: LGTM!

The platform-specific use_last_error handling correctly wraps the FFI call on Windows while cleanly suppressing the unused warning on other platforms.


1654-1681: LGTM!

The AsBuffer implementation correctly provides buffer access to function pointer data, with appropriate fallbacks when StgInfo is unavailable.


1770-1779: LGTM!

Adding the flags field to ThunkUserData enables proper errno handling during callback execution, matching CPython's callback behavior.


1828-1845: LGTM!

The platform-specific wchar_t handling correctly addresses the size difference (16-bit on Windows, 32-bit on Unix) and uses appropriate string conversion methods for each platform.


2032-2082: LGTM!

The PyCThunk::new function cleanly integrates the flags parameter, enabling per-callback errno handling and other flag-controlled behaviors.


1507-1527: LGTM!

The pycdata_from_ffi_result function safely copies FFI result data with proper bounds checking to prevent buffer overflows.


1310-1349: LGTM!

The build_callargs dispatcher cleanly routes to the appropriate argument builder based on the presence of argtypes, paramflags, and COM method context.


1743-1758: LGTM!

The _flags_ getter correctly prioritizes class-level attribute over StgInfo, enabling dynamic flag configuration via CDLL's _FuncPtr class pattern.


420-426: The format string "X{}" is intentional and correctly documented.

The review comment incorrectly assumes this code should follow Python's standard struct module conventions. However, the code explicitly documents this as RustPython's custom default: the comment on line 1658 states "Use default values for function pointers: format="X{}", size=sizeof(pointer)". This is RustPython's internal format representation, not standard Python ctypes. Throughout the ctypes implementation, custom format strings are used (e.g., "B" for unions/structures, "&{}{}" for pointers), indicating that "X{}" is an intentional design choice for function pointers in this implementation.

Likely an incorrect or invalid review comment.

Comment on lines 1437 to 1444
// Default: return as int
let val = match raw_result {
RawResult::Pointer(p) => *p as i32,
RawResult::Value(v) => *v as i32,
RawResult::Void => return None,
};
return Some(vm.ctx.new_int(val).into());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pointer truncation when returning as default int.

When restype is None or not a recognized type, pointer values are cast to i32 (lines 1439, 1459, 1473), which truncates 64-bit addresses. While this may match CPython's legacy behavior, it could cause subtle bugs on 64-bit systems.

Consider returning as isize or the full pointer value to preserve address fidelity, or document this limitation.

🔎 Proposed fix for default return
         None => {
             // Default: return as int
             let val = match raw_result {
-                RawResult::Pointer(p) => *p as i32,
-                RawResult::Value(v) => *v as i32,
+                RawResult::Pointer(p) => *p as isize,
+                RawResult::Value(v) => *v as i32,
                 RawResult::Void => return None,
             };
             return Some(vm.ctx.new_int(val).into());
         }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 47 to 59
// Set format string: "&<element_format>" or "&(shape)<element_format>" for arrays
if let Some(ref proto) = stg_info.proto {
let item_info = proto.stg_info_opt().expect("proto has StgInfo");
let current_format = item_info.format.as_deref().unwrap_or("B");
stg_info.format = Some(format!("&{}", current_format));
// Include shape for array types in the pointer format
let shape_str = if !item_info.shape.is_empty() {
let dims: Vec<String> = item_info.shape.iter().map(|d| d.to_string()).collect();
format!("({})", dims.join(","))
} else {
String::new()
};
stg_info.format = Some(format!("&{}{}", shape_str, current_format));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid panicking when _type_ has no StgInfo in PyCPointerType::init.

Here:

if let Some(ref proto) = stg_info.proto {
    let item_info = proto.stg_info_opt().expect("proto has StgInfo");}

_type_ can legitimately be an abstract/incomplete ctypes type without StgInfo yet, in which case this will panic instead of raising a Python exception. Better to skip format initialisation if there is no StgInfo:

-        if let Some(ref proto) = stg_info.proto {
-            let item_info = proto.stg_info_opt().expect("proto has StgInfo");
+        if let Some(ref proto) = stg_info.proto {
+            if let Some(item_info) = proto.stg_info_opt() {-            stg_info.format = Some(format!("&{}{}", shape_str, current_format));
+                stg_info.format = Some(format!("&{}{}", shape_str, current_format));
+            }
         }

You already validate typ_type.stg_info_opt().is_some() in set_type, so this change just makes the default init path tolerant of incomplete types.

🤖 Prompt for AI Agents
In crates/vm/src/stdlib/ctypes/pointer.rs around lines 47 to 59, avoid
unwrapping/expect on proto.stg_info_opt() which can be None for
abstract/incomplete ctypes types; instead check for presence and skip format
initialization when there is no StgInfo. Concretely, replace the expect with a
guarded branch (e.g., if let Some(item_info) = proto.stg_info_opt() { … } ) and
only compute shape_str and set stg_info.format inside that branch so the init
path does not panic and remains tolerant of incomplete types.

@youknowone youknowone marked this pull request as draft December 21, 2025 04:01
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