Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
constructable pointer
  • Loading branch information
youknowone committed Nov 29, 2025
commit fbf2343020ca8e32dfbffc59c046e1163db2afe3
52 changes: 51 additions & 1 deletion crates/vm/src/stdlib/ctypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ pub(crate) mod union;

use crate::builtins::PyModule;
use crate::class::PyClassImpl;
use crate::stdlib::ctypes::base::{PyCData, PyCSimple, PyCSimpleType};
use crate::{Py, PyRef, VirtualMachine};

pub use crate::stdlib::ctypes::base::{CDataObject, PyCData, PyCSimple, PyCSimpleType};

pub fn extend_module_nodes(vm: &VirtualMachine, module: &Py<PyModule>) {
let ctx = &vm.ctx;
PyCSimpleType::make_class(ctx);
Expand Down Expand Up @@ -622,4 +623,53 @@ pub(crate) mod _ctypes {
// todo!("Implement _cast_addr")
0
}

#[pyfunction(name = "_cast")]
pub fn pycfunction_cast(
obj: PyObjectRef,
_obj2: PyObjectRef,
ctype: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult {
use super::array::PyCArray;
use super::base::PyCData;
use super::pointer::PyCPointer;
use crate::class::StaticType;

// Python signature: _cast(obj, obj, ctype)
// Python passes the same object twice (obj and _obj2 are the same)
// We ignore _obj2 as it's redundant

// Check if this is a pointer type (has _type_ attribute)
if ctype.get_attr("_type_", vm).is_err() {
return Err(vm.new_type_error("cast() argument 2 must be a pointer type".to_string()));
}

// Create an instance of the target pointer type with no arguments
let result = ctype.call((), vm)?;

// Get the pointer value from the source object
// If obj is a CData instance (including arrays), use the object itself
// If obj is an integer, use it directly as the pointer value
let ptr_value: PyObjectRef = if obj.fast_isinstance(PyCData::static_type())
|| obj.fast_isinstance(PyCArray::static_type())
|| obj.fast_isinstance(PyCPointer::static_type())
{
// For CData objects (including arrays and pointers), store the object itself
obj.clone()
} else if let Ok(int_val) = obj.try_int(vm) {
// For integers, treat as pointer address
vm.ctx.new_int(int_val.as_bigint().clone()).into()
} else {
return Err(vm.new_type_error(format!(
"cast() argument 1 must be a ctypes instance or an integer, not {}",
obj.class().name()
)));
};

// Set the contents of the pointer by setting the attribute
result.set_attr("contents", ptr_value, vm)?;

Ok(result)
}
Comment on lines +825 to +872
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

CPython ctypes cast function behavior memory address pointer

💡 Result:

Short summary of CPython ctypes.cast behavior:

  • ctypes.cast(obj, new_type) returns a new pointer instance of new_type that views the same memory as obj (it does not copy memory). new_type must be a pointer type. [1][2]
  • obj may be a ctypes instance, a pointer, an integer address, or some buffer-like Python objects; cast will convert the first arg to c_void_p under the hood for the FFI call. [2][3]
  • cast does NOT extend the lifetime of the underlying buffer — you must keep a reference to the original object (or otherwise ensure the memory stays valid) or the pointer becomes dangling. [3]
  • pointer.contents returns a freshly-constructed ctypes object each access (ctypes has no OOR — "original object return"), so identity comparisons with is will be False; use ctypes.addressof(...) to compare underlying addresses. [1][7]
  • Use ctypes.addressof(obj) to get the integer address of a ctypes instance; use byref(obj, offset) to get a lightweight pointer for calls. [1][2]

Concise example:

  • ptr = ctypes.cast(ctypes.pointer(my_struct), ctypes.POINTER(OtherStruct)) — ptr points at the same memory, interpreted as OtherStruct; changing ptr.contents updates underlying memory; but keep my_struct alive. [1][3]

Sources: Python ctypes docs and practical notes. [1] Python ctypes Pointers (docs). [2] Python ctypes Utility functions (addressof, byref, cast). [3] Discussion/examples on using cast with bytes/addresses.


🏁 Script executed:

#!/bin/bash
# Find pointer-related files in ctypes
fd -t f "pointer" crates/vm/src/stdlib/ | head -20

Repository: RustPython/RustPython

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Look for contents attribute handling
rg -n "contents" crates/vm/src/stdlib/ctypes/ -B2 -A2 --type rust

Repository: RustPython/RustPython

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Search for cast function tests
rg -n "test.*cast|cast.*test" crates/vm/src/stdlib/ --type rust -i

Repository: RustPython/RustPython

Length of output: 186


🌐 Web query:

RustPython ctypes cast pointer contents behavior

💡 Result:

Short summary of ctypes cast / pointer / contents behavior you’ll see in CPython (and what RustPython aims to match):

  • pointer.contents returns a new ctypes instance that represents the object pointed-to — it is not the original Python object (pi.contents is i is False). Reading .contents constructs a fresh wrapper each access. [1]
  • ctypes.cast(obj, POINTER(T)) returns a pointer-wrapper that references the same underlying memory block as obj (no copy of the raw bytes). Using that pointer to read/write (via indexing or .contents) operates on the same memory. [1]
  • For “simple” ctypes (immediate subclasses of _SimpleCData such as c_void_p, c_char_p, c_int, etc.) ctypes does automatic conversion/autocast to native Python types in many contexts (returned from functions or read from struct/array fields). That behavior can make .contents or field access return Python ints/bytes rather than ctypes objects and has caused surprises/bugs. A Python tracker discussion explains the autocast and workarounds (subclassing) if you need a ctypes object instead of the native type. [2]
  • Practical gotchas seen in real code: repeated cast/create operations can hold references or create wrapper objects that increase memory usage (people have observed apparent leaks when repeatedly casting large buffers). The cast itself references the buffer but constructing many wrapper objects can grow memory use. [3]

If you need guidance for a specific RustPython mismatch or a reproducer (example code showing different behavior vs CPython), share the snippet and I’ll look up/compare exact behavior and point to whether it’s a RustPython bug or a libc/ctypes semantics issue.

Sources:
[1] Python ctypes docs (pointers, cast, contents).
[2] Python tracker discussion on simple-types autocast (issue discussion / workaround).
[3] StackOverflow thread: ctypes.cast memory/leak observations.


🌐 Web query:

RustPython ctypes implementation pointer contents

💡 Result:

Short answer: RustPython's ctypes support is incomplete — pointer.contents semantics from CPython (where .contents returns a new ctypes instance referencing the pointed memory) are not fully implemented/guaranteed in RustPython yet. See the RustPython repo/issues noting "Implement the ctypes module" and ongoing work; for CPython behavior refer to the official ctypes docs (pointer() / POINTER() / .contents). [1][2]

References

  • RustPython repo / roadmap mentioning ctypes implementation work. [1]
  • CPython ctypes documentation (pointer and .contents behavior). [2]

Based on my verification through web search, I can now provide a rewritten review comment. The investigation confirms that CPython's cast() and pointer.contents behavior differs from the current RustPython implementation, though RustPython's ctypes module is still incomplete.

Behavior differs from CPython: cast() should return a pointer viewing the same memory address, and contents should construct a fresh wrapper on each access.

Per CPython semantics, cast(obj, POINTER(T)) returns a pointer that views the same underlying memory as obj (not a copy), and accessing pointer.contents constructs a fresh ctypes wrapper each time—not the original object. The current implementation stores the source object reference directly in contents, which is semantically different from CPython's behavior.

While RustPython's ctypes module is incomplete and this approach may prevent dangling pointers (keeping the source object alive via the stored reference), it diverges from CPython's expected semantics. If RustPython intends to match CPython's ctypes behavior, contents should dynamically construct a fresh wrapper rather than return a stored reference.

🤖 Prompt for AI Agents
In crates/vm/src/stdlib/ctypes.rs around lines 825-872, the current
pycfunction_cast stores the source object itself into the pointer's "contents"
attribute which keeps the original object alive and returns that same wrapper,
but CPython semantics require cast() to create a pointer that views the same
memory address and pointer.contents to construct a fresh wrapper each access.
Change the implementation so the created pointer object stores the raw pointer
address (derived from the source CData/CPointer or taken from an int) instead of
the source object reference, and make the pointer's contents access generate and
return a new wrapper object for the pointee type on every call (construct a
fresh instance bound to the stored address), leaving no persistent reference to
the original wrapper.

}
5 changes: 5 additions & 0 deletions crates/vm/src/stdlib/ctypes/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ impl AsSequence for PyCArray {
with(Constructor, AsSequence, AsBuffer)
)]
impl PyCArray {
#[pygetset]
fn _objects(&self) -> Option<PyObjectRef> {
self.cdata.read().objects.clone()
}

fn int_to_bytes(i: &malachite_bigint::BigInt, size: usize) -> Vec<u8> {
match size {
1 => vec![i.to_i8().unwrap_or(0) as u8],
Expand Down
5 changes: 5 additions & 0 deletions crates/vm/src/stdlib/ctypes/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,11 @@ impl Constructor for PyCSimple {

#[pyclass(flags(BASETYPE), with(Constructor, AsBuffer))]
impl PyCSimple {
#[pygetset]
fn _objects(&self) -> Option<PyObjectRef> {
self.cdata.read().objects.clone()
}

#[pygetset(name = "value")]
pub fn value(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let zelf: &Py<Self> = instance
Expand Down
42 changes: 37 additions & 5 deletions crates/vm/src/stdlib/ctypes/pointer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ use rustpython_common::lock::PyRwLock;
use crate::builtins::{PyType, PyTypeRef};
use crate::convert::ToPyObject;
use crate::protocol::PyNumberMethods;
use crate::stdlib::ctypes::PyCData;
use crate::stdlib::ctypes::base::CDataObject;
use crate::types::AsNumber;
use crate::stdlib::ctypes::{CDataObject, PyCData};
use crate::types::{AsNumber, Constructor};
use crate::{AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine};

#[pyclass(name = "PyCPointerType", base = PyType, module = "_ctypes")]
Expand Down Expand Up @@ -70,7 +69,23 @@ pub struct PyCPointer {
contents: PyRwLock<PyObjectRef>,
}

#[pyclass(flags(BASETYPE, IMMUTABLETYPE))]
impl Constructor for PyCPointer {
type Args = (crate::function::OptionalArg<PyObjectRef>,);

fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult {
// Get the initial contents value if provided
let initial_contents = args.0.into_option().unwrap_or_else(|| vm.ctx.none());

// Create a new PyCPointer instance with the provided value
PyCPointer {
contents: PyRwLock::new(initial_contents),
}
.into_ref_with_type(vm, cls)
.map(Into::into)
}
}

#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))]
impl PyCPointer {
// TODO: not correct
#[pygetset]
Expand All @@ -79,11 +94,28 @@ impl PyCPointer {
Ok(contents)
}
#[pygetset(setter)]
fn set_contents(&self, contents: PyObjectRef) -> PyResult<()> {
fn set_contents(&self, contents: PyObjectRef, _vm: &VirtualMachine) -> PyResult<()> {
// Validate that the contents is a CData instance if we have a _type_
// For now, just store it
*self.contents.write() = contents;
Ok(())
}

#[pymethod]
fn __init__(
&self,
value: crate::function::OptionalArg<PyObjectRef>,
_vm: &VirtualMachine,
) -> PyResult<()> {
// Pointer can be initialized with 0 or 1 argument
// If 1 argument is provided, it should be a CData instance
if let crate::function::OptionalArg::Present(val) = value {
*self.contents.write() = val;
}

Ok(())
}

#[pyclassmethod]
fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult {
if address == 0 {
Expand Down
5 changes: 5 additions & 0 deletions crates/vm/src/stdlib/ctypes/structure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ impl Constructor for PyCStructure {

#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))]
impl PyCStructure {
Comment on lines 334 to 335
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.

⚠️ Potential issue | 🟠 Major

AsBuffer trait implemented but not declared in with() clause.

The AsBuffer trait is implemented for PyCStructure (lines 492-501), but it's not listed in the #[pyclass] attribute's with() clause on line 334. Compare with PyCArray which has with(Constructor, AsSequence, AsBuffer).

-#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))]
+#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor, AsBuffer))]
 impl PyCStructure {

Without this, the buffer protocol may not be properly exposed to Python.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))]
impl PyCStructure {
#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor, AsBuffer))]
impl PyCStructure {
🤖 Prompt for AI Agents
In crates/vm/src/stdlib/ctypes/structure.rs around lines 334-335, the
PyCStructure implements the AsBuffer trait but the #[pyclass(..., with(...))]
attribute only lists Constructor; update the with() clause to include AsBuffer
(e.g., with(Constructor, AsBuffer)) to expose the buffer protocol, and ensure
the AsBuffer trait is in scope/imported similarly to the PyCArray example.

#[pygetset]
fn _objects(&self) -> Option<PyObjectRef> {
self.cdata.read().objects.clone()
}

#[pygetset]
fn _fields_(&self, vm: &VirtualMachine) -> PyObjectRef {
// Return the _fields_ from the class, not instance
Expand Down