Preserve ctypes element views and their references (#9229)

* Preserve ctypes element views and their references

Assisted-by: Codex:GPT-6

* Fix ctypes reentrant ownership and scalar view handling

Assisted-by: Codex:GPT-6

* Fix ctypes pointer view writes and indexed object lookup

Assisted-by: Codex:GPT-6

* Fix ctypes field byte order and encoded instance copies

Assisted-by: Codex:GPT-6

* Fix ctypes effective field types and overlapping pointer copies

Assisted-by: Codex:GPT-6

* Reject arrays for incomplete ctypes pointer fields

Assisted-by: Codex:GPT-6
This commit is contained in:
1ndahous3 authored and GitHub committed 2026-10-20 09:37:33 +00:00
1 parent c3b8ce79c8
commit 980edd8b57
7 files changed
+807 -285

No files matched your search

+129 -120
View File
@@ -545,23 +545,51 @@ impl PyCArray {
let index = index as usize;
let element_size = stg.as_ref().map_or(0, |i| i.element_size);
let offset = index * element_size;
if let Some(element_type) = stg.as_ref().and_then(|info| info.element_type.as_ref())
&& !super::simple::returns_scalar(element_type)
{
let length = element_type.stg_info(vm)?.length;
let buffer = zelf.0.buffer.read();
let Some(data) = buffer.get(offset..offset + element_size) else {
drop(buffer);
return Err(vm.new_index_error("invalid index"));
};
// SAFETY: this element lies within the array's live buffer. The view
// retains the array; ctypes users must still avoid reallocating or
// concurrently accessing the same native memory through aliases.
let view = unsafe {
PyCData::from_base_obj(
data.as_ptr().cast_mut(),
element_size,
zelf.to_owned().into(),
index,
)
};
view.base_offset.store(offset);
view.length.store(length);
drop(buffer);
return view.into_view(element_type.clone(), vm);
}
let type_code = Self::get_element_type_code(zelf, vm);
// Get target buffer and offset (base's buffer if available, otherwise own)
let base_obj = zelf.0.base.read().clone();
let (buffer_lock, final_offset) = if let Some(cdata) = base_obj
.as_ref()
.and_then(|b| b.downcast_ref::<super::PyCData>())
let buffer = zelf.0.buffer.read();
if type_code.as_deref() == Some("O") {
let address = rustpython_host_env::ctypes::read_pointer_from_buffer(
&buffer[offset..offset + element_size],
);
drop(buffer);
return zelf.0.object_at(address, Some(index), vm);
}
if let Some(element_type) = stg.as_ref().and_then(|info| info.element_type.as_ref())
&& super::simple::is_swapped(element_type)
{
(&cdata.buffer, zelf.0.base_offset.load() + offset)
} else {
(&zelf.0.buffer, offset)
};
let buffer = buffer_lock.read();
let mut bytes = buffer[offset..offset + element_size].to_vec();
bytes.reverse();
return Ok(super::base::bytes_to_pyobject(element_type, &bytes, vm));
}
Ok(Self::read_element_from_buffer(
&buffer,
final_offset,
offset,
element_size,
type_code.as_deref(),
vm,
@@ -589,10 +617,7 @@ impl PyCArray {
}
/// Helper to write an element value to a buffer at given offset
/// This is extracted to share code between direct write and base-buffer write
#[allow(clippy::too_many_arguments)]
fn write_element_to_buffer(
buffer: &mut [u8],
offset: usize,
element_size: usize,
type_code: Option<&str>,
@@ -601,20 +626,22 @@ impl PyCArray {
index: usize,
vm: &VirtualMachine,
) -> PyResult<()> {
// Conversions and reference replacement may call Python. Only the raw
// storage write runs under the buffer lock.
let write = |value| {
let mut buffer = zelf.0.buffer.write();
write_array_element(cow_bytes_as_mut(&mut buffer), offset, value);
};
match type_code {
Some("c") => {
if let Some(b) = value.downcast_ref::<PyBytes>() {
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Byte(b.as_bytes().first().copied().unwrap_or(0)),
);
write(ArrayElementWriteValue::Byte(
b.as_bytes().first().copied().unwrap_or(0),
));
} else if let Ok(int_val) = value.try_int(vm) {
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Byte(int_val.as_bigint().to_u8().unwrap_or(0)),
);
write(ArrayElementWriteValue::Byte(
int_val.as_bigint().to_u8().unwrap_or(0),
));
} else {
return Err(vm.new_type_error("an integer or bytes of length 1 is required"));
}
@@ -622,17 +649,18 @@ impl PyCArray {
Some("u") => {
if let Some(s) = value.downcast_ref::<PyStr>() {
let code = s.as_wtf8().code_points().next().map_or(0, |c| c.to_u32());
write_array_element(buffer, offset, ArrayElementWriteValue::Wchar(code));
write(ArrayElementWriteValue::Wchar(code));
} else {
return Err(vm.new_type_error("unicode string expected"));
}
}
Some("z") => {
let mut hidden = None;
let (ptr_val, converted) = if value.is(&vm.ctx.none) {
(0usize, None)
} else if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
zelf.0.keep_alive(index, kept_alive);
hidden = Some(kept_alive);
(ptr, Some(value.to_owned()))
} else if let Some(simple) = value.downcast_ref::<super::PyCSimple>()
&& value.class().type_code(vm).as_deref() == Some("z")
@@ -650,16 +678,12 @@ impl PyCArray {
value.class().name()
)));
};
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Pointer {
value: ptr_val,
size: element_size,
},
);
write(ArrayElementWriteValue::Pointer {
value: ptr_val,
size: element_size,
});
if let Some(c) = converted {
return zelf.0.keep_ref(index, c, vm);
return zelf.0.keep_ref_with_hidden(index, c, hidden, vm);
}
}
Some("Z") => {
@@ -681,18 +705,40 @@ impl PyCArray {
} else {
return Err(vm.new_type_error("unicode string or integer address expected"));
};
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Pointer {
value: ptr_val,
size: element_size,
},
);
write(ArrayElementWriteValue::Pointer {
value: ptr_val,
size: element_size,
});
if let Some(c) = converted {
return zelf.0.keep_ref(index, c, vm);
}
}
Some("O") => {
// A py_object instance contributes its stored reference, while
// other Python objects are the value itself.
if value.downcast_ref::<PyCData>().is_none() {
write(ArrayElementWriteValue::Pointer {
value: value.get_id(),
size: element_size,
});
return zelf.0.keep_ref(index, value.to_owned(), vm);
}
let stg = zelf.class().stg_info(vm)?;
let element_type = stg.element_type.as_ref().unwrap();
if !value.fast_isinstance(element_type) {
return Err(vm.new_type_error(format!(
"expected {} instance, not {}",
element_type.name(),
value.class().name()
)));
}
let cdata = value.downcast_ref::<PyCData>().unwrap();
let bytes = cdata.buffer.read().to_vec();
write(ArrayElementWriteValue::Bytes {
bytes: &bytes,
size: element_size,
});
}
Some("f") => {
// c_float: convert int/float to f32 bytes
let f32_val = if let Ok(float_val) = value.try_float(vm) {
@@ -702,14 +748,10 @@ impl PyCArray {
} else {
return Err(vm.new_type_error("a float is required"));
};
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Float {
value: f32_val.into(),
size: 4,
},
);
write(ArrayElementWriteValue::Float {
value: f32_val.into(),
size: 4,
});
}
Some("d" | "g") => {
// c_double / c_longdouble: convert int/float to f64 bytes
@@ -720,39 +762,27 @@ impl PyCArray {
} else {
return Err(vm.new_type_error("a float is required"));
};
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Float {
value: f64_val,
size: 8,
},
);
write(ArrayElementWriteValue::Float {
value: f64_val,
size: 8,
});
// For "g" type, remaining bytes stay zero
}
_ => {
// Handle ctypes instances (copy their buffer)
if let Some(cdata) = value.downcast_ref::<PyCData>() {
let src_buffer = cdata.buffer.read();
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Bytes {
bytes: &src_buffer,
size: element_size,
},
);
let src_buffer = cdata.buffer.read().to_vec();
write(ArrayElementWriteValue::Bytes {
bytes: &src_buffer,
size: element_size,
});
// Other types: use int_to_bytes
} else if let Ok(int_val) = value.try_int(vm) {
let bytes = Self::int_to_bytes(int_val.as_bigint(), element_size);
write_array_element(
buffer,
offset,
ArrayElementWriteValue::Bytes {
bytes: &bytes,
size: element_size,
},
);
write(ArrayElementWriteValue::Bytes {
bytes: &bytes,
size: element_size,
});
} else {
return Err(vm.new_type_error(format!(
"expected {} instance, not {}",
@@ -765,8 +795,7 @@ impl PyCArray {
// KeepRef
if super::base::PyCData::should_keep_ref(value) {
let to_keep = super::base::PyCData::get_kept_objects(value, vm);
zelf.0.keep_ref(index, to_keep, vm)?;
zelf.0.keep_cdata(index, value, vm)?;
}
Ok(())
@@ -789,49 +818,29 @@ impl PyCArray {
let offset = index * element_size;
let type_code = Self::get_element_type_code(zelf, vm);
// Get target buffer and offset (base's buffer if available, otherwise own)
let base_obj = zelf.0.base.read().clone();
let (buffer_lock, final_offset) = if let Some(cdata) = base_obj
.as_ref()
.and_then(|b| b.downcast_ref::<super::PyCData>())
if let Some(element_type) = stg.as_ref().and_then(|info| info.element_type.as_ref())
&& super::simple::is_swapped(element_type)
&& value.downcast_ref::<PyCData>().is_none()
{
(&cdata.buffer, zelf.0.base_offset.load() + offset)
} else {
(&zelf.0.buffer, offset)
};
let mut buffer = buffer_lock.write();
// For shared memory (Cow::Borrowed), we need to write directly to the memory
// For owned memory (Cow::Owned), we can write to the owned buffer
match &mut *buffer {
Cow::Borrowed(slice) => {
// SAFETY: For from_buffer, the slice points to writable shared memory.
// Python's from_buffer requires writable buffer, so this is safe.
let owned_slice =
unsafe { rustpython_host_env::ctypes::borrowed_slice_as_mut(slice) };
Self::write_element_to_buffer(
owned_slice,
final_offset,
element_size,
type_code.as_deref(),
value,
zelf,
index,
vm,
)
}
Cow::Owned(vec) => Self::write_element_to_buffer(
vec,
final_offset,
element_size,
type_code.as_deref(),
value,
zelf,
index,
vm,
),
let type_code = type_code
.as_deref()
.ok_or_else(|| vm.new_type_error("no _type_ attribute"))?;
let bytes = super::simple::swapped_value_bytes(type_code, value, vm)?;
zelf.0.write_bytes_at_offset(offset, &bytes);
return Ok(());
}
// A view's buffer already points to its element. Its base can be a
// pointer object, whose own buffer stores an address rather than data.
Self::write_element_to_buffer(
offset,
element_size,
type_code.as_deref(),
value,
zelf,
index,
vm,
)
}
// Array_subscript
+244 -52
View File
@@ -5,6 +5,7 @@ use crate::builtins::{
};
use crate::class::StaticType;
use crate::convert::ToPyObject;
use crate::dict_inner::DictKey;
use crate::function::{ArgBytesLike, ArgStrictInt, OptionalArg, PySetterValue};
use crate::protocol::{BufferMethods, PyBuffer};
use crate::types::{Constructor, GetDescriptor, Representable};
@@ -439,6 +440,36 @@ pub struct PyCData {
pub(super) kept_refs: PyRwLock<std::collections::HashMap<String, PyObjectRef>>,
}
// Retention metadata is user-mutable. Its fast lookup must not call Python
// equality on a colliding key; non-exact string keys use the graph fallback.
struct ObjectKey<'a>(&'a str);
impl DictKey for ObjectKey<'_> {
type Owned = String;
fn _to_owned(&self, _vm: &VirtualMachine) -> String {
self.0.to_owned()
}
fn key_hash(&self, vm: &VirtualMachine) -> PyResult<crate::common::hash::PyHash> {
self.0.key_hash(vm)
}
fn key_is(&self, _other: &PyObject) -> bool {
false
}
fn key_eq(&self, vm: &VirtualMachine, other: &PyObject) -> PyResult<bool> {
Ok(other
.downcast_ref_if_exact::<PyStr>(vm)
.is_some_and(|key| key.as_wtf8() == self.0))
}
fn key_as_isize(&self, vm: &VirtualMachine) -> PyResult<isize> {
self.0.key_as_isize(vm)
}
}
impl PyCData {
/// Create from StgInfo (PyCData_MallocBuffer pattern)
pub(crate) fn from_stg_info(stg_info: &StgInfo) -> Self {
@@ -696,9 +727,18 @@ impl PyCData {
pub fn unique_key(&self, index: usize) -> String {
let mut key = format!("{index:x}");
// Walk up the base chain to build hierarchical key
if self.base.read().is_some() {
let parent_index = self.index.load();
key.push_str(&format!(":{parent_index:x}"));
let mut base = self.base.read().clone();
if base.is_some() {
key.push_str(&format!(":{:x}", self.index.load()));
}
while let Some(parent) = base.take() {
let Some(cdata) = parent.downcast_ref::<Self>() else {
break;
};
base.clone_from(&cdata.base.read());
if base.is_some() {
key.push_str(&format!(":{:x}", cdata.index.load()));
}
}
key
}
@@ -758,8 +798,11 @@ impl PyCData {
*objects = Some(dict.into());
}
// Replacing a reference can run a finalizer that accesses this object.
let dict_obj = objects.clone();
drop(objects);
// Store in dict with unique key
if let Some(dict_obj) = objects.as_ref()
if let Some(dict_obj) = dict_obj
&& let Some(dict) = dict_obj.downcast_ref::<PyDict>()
{
let key_obj: PyObjectRef = vm.ctx.new_str(key).into();
@@ -774,33 +817,49 @@ impl PyCData {
/// lives as long as the owning ctypes object.
/// Uses unique_key (hierarchical) so nested fields don't collide.
pub fn keep_alive(&self, index: usize, obj: PyObjectRef) {
drop(self.replace_hidden(index, Some(obj)));
}
fn replace_hidden(&self, index: usize, obj: Option<PyObjectRef>) -> Option<PyObjectRef> {
let key = self.unique_key(index);
let base = self.base.read().clone();
if let Some(base_obj) = base {
let root = Self::find_root_object(&base_obj);
if let Some(cdata) = root.downcast_ref::<Self>() {
cdata.kept_refs.write().insert(key, obj);
return;
}
let root = base.as_ref().map(|base| Self::find_root_object(base));
let cdata = root
.as_ref()
.and_then(|root| root.downcast_ref::<Self>())
.map_or(self, |cdata| cdata);
let mut refs = cdata.kept_refs.write();
match obj {
Some(obj) => refs.insert(key, obj),
None => refs.remove(&key),
}
self.kept_refs.write().insert(key, obj);
}
pub(super) fn keep_ref_with_hidden(
&self,
index: usize,
keep: PyObjectRef,
hidden: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
// Publish all owners before dropping the old visible reference: its
// finalizer may overwrite this same slot. Keep old storage alive until
// then, and do not overwrite the finalizer's owners afterwards.
let _old_hidden = self.replace_hidden(index, hidden);
self.keep_ref(index, keep, vm)
}
/// Find the root object (one without a base) by walking up the base chain
fn find_root_object(obj: &PyObject) -> PyObjectRef {
// Try to get base from different ctypes types
let base = if let Some(cdata) = obj.downcast_ref::<Self>() {
cdata.base.read().clone()
} else {
None
};
// Recurse if there's a base, otherwise this is the root
if let Some(base_obj) = base {
Self::find_root_object(&base_obj)
} else {
obj.to_owned()
let mut root = obj.to_owned();
while let Some(cdata) = root.downcast_ref::<Self>() {
let base = cdata.base.read().clone();
match base {
Some(base) if base.downcast_ref::<Self>().is_some() => root = base,
_ => break,
}
}
root
}
/// Store a value in an object's _objects dict with the given key
@@ -834,8 +893,10 @@ impl PyCData {
*objects = Some(dict.into());
}
let dict_obj = objects.clone();
drop(objects);
// Store in dict
if let Some(dict_obj) = objects.as_ref()
if let Some(dict_obj) = dict_obj
&& let Some(dict) = dict_obj.downcast_ref::<PyDict>()
{
let key_obj: PyObjectRef = vm.ctx.new_str(key).into();
@@ -846,14 +907,121 @@ impl PyCData {
}
/// Get kept objects from a CData instance
/// Returns the _objects of the CData, or an empty dict if None.
/// Returns the _objects of the root CData, or an empty dict if None.
pub fn get_kept_objects(value: &PyObject, vm: &VirtualMachine) -> PyObjectRef {
value
Self::find_root_object(value)
.downcast_ref::<Self>()
.and_then(|cdata| cdata.objects.read().clone())
.unwrap_or_else(|| vm.ctx.new_dict().into())
}
// Copying a view must retain its root's references, including private
// backing allocations used for null-terminated strings.
pub(super) fn keep_cdata(
&self,
index: usize,
value: &PyObject,
vm: &VirtualMachine,
) -> PyResult<()> {
let root = Self::find_root_object(value);
let hidden: Vec<_> = root
.downcast_ref::<Self>()
.map(|cdata| cdata.kept_refs.read().values().cloned().collect())
.unwrap_or_default();
let hidden = (!hidden.is_empty()).then(|| vm.ctx.new_tuple(hidden).into());
self.keep_ref_with_hidden(index, Self::get_kept_objects(&root, vm), hidden, vm)
}
// A scalar owns one reference; a scalar view stores it in its root instead.
pub(super) fn keep_value(
&self,
value: PyObjectRef,
hidden: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
let _old_hidden = self.replace_hidden(0, hidden);
if self.base.read().is_some() {
self.keep_ref(0, value, vm)
} else {
let old = self.objects.write().replace(value);
drop(old);
Ok(())
}
}
// Resolve a py_object through owned references, never by turning an
// arbitrary integer from native memory into a Rust reference. Views can
// reach the object through nested _objects dictionaries or cast cycles.
pub(super) fn object_at(
&self,
address: usize,
index: Option<usize>,
vm: &VirtualMachine,
) -> PyResult {
if address == 0 {
return Err(vm.new_value_error("PyObject is NULL"));
}
if address == vm.ctx.none.get_id() {
return Ok(vm.ctx.none());
}
let objects = self.objects.read().clone();
if let Some(obj) = &objects
&& obj.get_id() == address
{
return Ok(obj.clone());
}
let base = self.base.read().clone();
let root = base.as_ref().map(|base| Self::find_root_object(base));
let owners = root
.as_ref()
.and_then(|root| root.downcast_ref::<Self>())
.and_then(|root| root.objects.read().clone())
.or_else(|| objects.clone());
if let Some(dict) = owners.as_ref().and_then(|obj| obj.downcast_ref::<PyDict>()) {
let key = self.unique_key(index.unwrap_or(0));
// A scalar view's setter stores under "0:<view path>", whereas
// assignment through its parent stores under "<view path>".
let parent_key = if index.is_none() {
key.strip_prefix("0:")
} else {
None
};
for key in core::iter::once(key.as_str()).chain(parent_key) {
if let Some(obj) = dict.inner_getitem_opt(&ObjectKey(key), vm)?
&& obj.get_id() == address
{
return Ok(obj);
}
}
}
// Casts, copied aggregates and buffer views can retain the referent
// through other keys or containers. Search those only on a direct miss.
let mut pending: Vec<_> = objects.into_iter().collect();
pending.extend(base);
pending.extend(self.kept_refs.read().values().cloned());
let mut visited = std::collections::HashSet::new();
while let Some(obj) = pending.pop() {
if obj.get_id() == address {
return Ok(obj);
}
if !visited.insert(obj.get_id()) {
continue;
}
if let Some(cdata) = obj.downcast_ref::<Self>() {
pending.extend(cdata.objects.read().iter().cloned());
pending.extend(cdata.base.read().iter().cloned());
pending.extend(cdata.kept_refs.read().values().cloned());
} else if let Some(dict) = obj.downcast_ref::<PyDict>() {
pending.extend(dict.values_vec());
} else if let Some(tuple) = obj.downcast_ref::<PyTuple>() {
pending.extend(tuple.as_slice().iter().cloned());
} else if let Some(view) = obj.downcast_ref::<PyMemoryView>() {
pending.push(view.viewed_object().to_owned());
}
}
Err(vm.new_value_error("PyObject is NULL"))
}
/// Check if a value should be stored in _objects
/// Returns true for ctypes objects and bytes (for c_char_p)
pub(crate) fn should_keep_ref(value: &PyObject) -> bool {
@@ -905,12 +1073,35 @@ impl PyCData {
}
}
// A compatible ctypes instance already contains the encoded field bytes.
// Snapshot them before writing: source and destination may share storage.
if let Some(proto_type) = proto.downcast_ref::<PyType>()
&& value.class().fast_issubclass(proto_type)
&& let Some(source) = value.downcast_ref::<Self>()
{
let bytes = {
let buffer = source.buffer.read();
rustpython_host_env::ctypes::copy_to_sized_bytes(&buffer, size)
};
self.write_bytes_at_offset(offset, &bytes);
return self.keep_cdata(index, &value, vm);
}
// Special handling for Pointer fields with Array values
if let Some(proto_type) = proto.downcast_ref::<PyType>()
&& proto_type
.class()
.fast_issubclass(super::pointer::PyCPointerType::static_type())
&& let Some(array) = value.downcast_ref::<super::array::PyCArray>()
&& {
let target_info = proto_type.stg_info(vm)?;
let array_info = array.class().stg_info(vm)?;
target_info
.proto
.as_deref()
.zip(array_info.element_type.as_deref())
.is_some_and(|(target, element)| target.is(element))
}
{
let buffer_addr = {
let array_buffer = array.0.buffer.read();
@@ -922,6 +1113,17 @@ impl PyCData {
return Ok(());
}
if value.downcast_ref::<Self>().is_some() {
return Err(vm.new_type_error(format!(
"incompatible types, {} instance instead of {} instance",
value.class().name(),
proto
.downcast_ref::<PyType>()
.ok_or_else(|| vm.new_type_error("field proto is not a type"))?
.name()
)));
}
// For array fields with tuple/list input, instantiate the array type
// and unpack elements as positional args (Array_init expects *args)
if let Some(proto_type) = proto.downcast_ref::<PyType>()
@@ -975,9 +1177,7 @@ impl PyCData {
let result =
rustpython_host_env::ctypes::pointer_to_sized_bytes_endian(ptr, size, needs_swap);
self.write_bytes_at_offset(offset, &result);
self.keep_ref(index, value, vm)?;
self.keep_alive(index, kept_alive);
return Ok(());
return self.keep_ref_with_hidden(index, value, Some(kept_alive), vm);
}
let (mut bytes, converted_value) = if let Some(type_code) = &field_type_code {
@@ -997,13 +1197,22 @@ impl PyCData {
if let Some(converted) = converted_value {
self.keep_ref(index, converted, vm)?;
} else if Self::should_keep_ref(&value) {
let to_keep = Self::get_kept_objects(&value, vm);
self.keep_ref(index, to_keep, vm)?;
self.keep_cdata(index, &value, vm)?;
}
Ok(())
}
pub(super) fn into_view(self, cls: PyTypeRef, vm: &VirtualMachine) -> PyResult {
if cls.fast_issubclass(super::function::PyCFuncPtr::static_type()) {
super::function::PyCFuncPtr::from_cdata(self)
.into_ref_with_type(vm, cls)
.map(Into::into)
} else {
self.into_ref_with_type(vm, cls).map(Into::into)
}
}
/// PyCData_get
/// Gets a field value at the given offset
pub fn get_field(
@@ -1080,15 +1289,7 @@ impl PyCData {
// Simple types: return primitive value
if proto_metaclass.fast_issubclass(super::simple::PyCSimpleType::static_type()) {
// Check for byte swapping
let needs_swap = base_obj
.class()
.as_object()
.get_attr("_swappedbytes_", vm)
.is_ok()
|| proto_type
.as_object()
.get_attr("_swappedbytes_", vm)
.is_ok();
let needs_swap = super::simple::is_swapped(&proto_type);
let data = if needs_swap && size > 1 {
let mut swapped = buffer_data;
@@ -1108,8 +1309,9 @@ impl PyCData {
if proto_metaclass.fast_issubclass(super::structure::PyCStructType::static_type())
|| proto_metaclass.fast_issubclass(super::union::PyCUnionType::static_type())
|| proto_metaclass.fast_issubclass(super::pointer::PyCPointerType::static_type())
|| proto_type.fast_issubclass(super::function::PyCFuncPtr::static_type())
{
cdata_obj.into_ref_with_type(vm, proto_type).map(Into::into)
cdata_obj.into_view(proto_type, vm)
} else {
// Fallback
Ok(vm.ctx.new_int(0).into())
@@ -1735,17 +1937,7 @@ impl Py<PyCField> {
match value {
PySetterValue::Assign(value) => {
// Check if needs byte swapping
let needs_swap = (obj
.class()
.as_object()
.get_attr("_swappedbytes_", vm)
.is_ok()
|| zelf
.proto
.as_object()
.get_attr("_swappedbytes_", vm)
.is_ok())
&& size > 1;
let needs_swap = super::simple::is_swapped(&zelf.proto) && size > 1;
// PyCData_set
cdata.set_field(
+26 -18
View File
@@ -645,6 +645,24 @@ pub(super) fn cast_impl(
}
impl PyCFuncPtr {
pub(super) fn from_cdata(data: PyCData) -> Self {
Self {
_base: data,
thunk: PyRwLock::new(None),
callable: PyRwLock::new(None),
converters: PyRwLock::new(None),
argtypes: PyRwLock::new(None),
restype: PyRwLock::new(None),
checker: PyRwLock::new(None),
errcheck: PyRwLock::new(None),
#[cfg(windows)]
index: PyRwLock::new(None),
#[cfg(windows)]
iid: PyRwLock::new(None),
paramflags: PyRwLock::new(None),
}
}
/// Get function pointer address from buffer
fn get_func_ptr(&self) -> usize {
let buffer = self._base.buffer.read();
@@ -670,23 +688,9 @@ impl Constructor for PyCFuncPtr {
let ptr_size = pointer_size();
if args.args.is_empty() {
return Self {
_base: PyCData::from_bytes(vec![0u8; ptr_size], None),
thunk: PyRwLock::new(None),
callable: PyRwLock::new(None),
converters: PyRwLock::new(None),
argtypes: PyRwLock::new(None),
restype: PyRwLock::new(None),
checker: PyRwLock::new(None),
errcheck: PyRwLock::new(None),
#[cfg(windows)]
index: PyRwLock::new(None),
#[cfg(windows)]
iid: PyRwLock::new(None),
paramflags: PyRwLock::new(None),
}
.into_ref_with_type(vm, cls)
.map(Into::into);
return Self::from_cdata(PyCData::from_bytes(vec![0u8; ptr_size], None))
.into_ref_with_type(vm, cls)
.map(Into::into);
}
let first_arg = &args.args[0];
@@ -847,9 +851,13 @@ impl Constructor for PyCFuncPtr {
// Store the thunk as a Python object to keep it alive
let thunk_ref: PyRef<PyCThunk> = thunk.into_ref(&vm.ctx);
let base = PyCData::from_bytes(Self::make_ptr_buffer(ptr_val), None);
let objects = vm.ctx.new_dict();
objects.set_item("0", thunk_ref.clone().into(), vm)?;
*base.objects.write() = Some(objects.into());
return Self {
_base: PyCData::from_bytes(Self::make_ptr_buffer(ptr_val), None),
_base: base,
thunk: PyRwLock::new(Some(thunk_ref)),
callable: PyRwLock::new(Some(first_arg.clone())),
converters: PyRwLock::new(None),
+87 -29
View File
@@ -288,13 +288,7 @@ impl PyCPointer {
/// Set the pointer value in buffer
pub(crate) fn set_ptr_value(&self, value: usize) {
let mut buffer = self.0.buffer.write();
rustpython_host_env::ctypes::write_pointer_to_buffer_at(
buffer.to_mut(),
0,
rustpython_host_env::ctypes::pointer_size(),
value,
);
self.0.write_bytes_at_offset(0, &value.to_ne_bytes());
}
// contents getter - reads address from b_ptr and creates an instance of the pointed-to type
@@ -313,12 +307,13 @@ impl PyCPointer {
.stg_info_opt()
.map_or_else(rustpython_host_env::ctypes::pointer_size, |info| info.size);
// Create instance that references the memory directly
// PyCData.into_ref_with_type works for all ctypes (simple, structure, union, array, pointer)
let cdata = unsafe { super::base::PyCData::at_address(ptr_val as *const u8, element_size) };
cdata
.into_ref_with_type(vm, proto_type.to_owned())
.map(Into::into)
// SAFETY: the pointer must designate valid native memory. Retaining it
// keeps Python-owned referents alive, but cannot validate arbitrary addresses.
let cdata = unsafe {
PyCData::from_base_obj(ptr_val as *mut u8, element_size, zelf.to_owned().into(), 0)
};
cdata.length.store(proto_type.stg_info(vm)?.length);
cdata.into_view(proto_type.to_owned(), vm)
}
// contents setter - stores address in b_ptr and keeps reference
@@ -400,11 +395,30 @@ impl PyCPointer {
// offset = index * iteminfo->size
let addr = pointer_item_address(ptr_value, index, element_size);
// Check if it's a simple type (has _type_ attribute)
if let Ok(type_attr) = proto_type.as_object().get_attr("_type_", vm)
&& let Ok(type_str) = type_attr.str(vm)
// Direct simple types and their generated endian variants return a
// scalar; user subclasses and aggregates return a view.
if super::simple::returns_scalar(proto_type)
&& let Ok(type_attr) = proto_type.as_object().get_attr("_type_", vm)
&& let Some(type_str) = type_attr.downcast_ref::<PyStr>()
{
let type_code = type_str.to_string();
if type_code == "O" || super::simple::is_swapped(proto_type) {
// SAFETY: as for ordinary pointer indexing below, the caller
// must supply a valid address covering the element's storage.
let bytes = unsafe {
rustpython_host_env::ctypes::borrow_memory(addr as *const u8, element_size)
};
if type_code == "O" {
return zelf.0.object_at(
rustpython_host_env::ctypes::read_pointer_from_buffer(bytes),
Some(index as usize),
vm,
);
}
let mut bytes = bytes.to_vec();
bytes.reverse();
return Ok(super::base::bytes_to_pyobject(proto_type, &bytes, vm));
}
return Ok(Self::read_value_at_address(
addr,
element_size,
@@ -415,11 +429,19 @@ impl PyCPointer {
// Complex type: create instance that references the memory directly (not a copy)
// This allows p[i].val = x to modify the original memory
// PyCData.into_ref_with_type works for all ctypes (array, structure, union, pointer)
let cdata = unsafe { super::base::PyCData::at_address(addr as *const u8, element_size) };
cdata
.into_ref_with_type(vm, proto_type.to_owned())
.map(Into::into)
// SAFETY: pointer indexing relies on the caller providing valid native
// memory. Retaining the pointer keeps its Python-owned referents alive,
// but does not make arbitrary addresses or concurrent native access safe.
let cdata = unsafe {
PyCData::from_base_obj(
addr as *mut u8,
element_size,
zelf.to_owned().into(),
index as usize,
)
};
cdata.length.store(proto_type.stg_info(vm)?.length);
cdata.into_view(proto_type.to_owned(), vm)
}
// Pointer_subscript slice handling (manual parsing, not PySlice_Unpack)
@@ -575,15 +597,30 @@ impl PyCPointer {
// Write value at address
// Handle Structure/Array types by copying their buffer
if type_code.as_deref() == Some("O")
&& value.downcast_ref::<PyCData>().is_some()
&& !value.fast_isinstance(proto_type)
{
return Err(vm.new_type_error(format!(
"expected {} instance, not {}",
proto_type.name(),
value.class().name()
)));
}
if let Some(cdata) = value.downcast_ref::<super::PyCData>()
&& (cdata.fast_isinstance(PyCStructure::static_type())
|| cdata.fast_isinstance(PyCArray::static_type())
|| cdata.fast_isinstance(PyCSimple::static_type()))
{
let src_buffer = cdata.buffer.read();
// The value may be a view of this element or overlapping storage.
let bytes = {
let buffer = cdata.buffer.read();
rustpython_host_env::ctypes::copy_to_sized_bytes(&buffer, element_size)
};
unsafe {
rustpython_host_env::ctypes::copy_bytes_to_address(addr, &src_buffer, element_size);
rustpython_host_env::ctypes::copy_bytes_to_address(addr, &bytes, element_size);
}
zelf.0.keep_cdata(index as usize, &value, vm)
} else {
// Handle z/Z specially to store converted value
if type_code.as_deref() == Some("z")
@@ -597,8 +634,12 @@ impl PyCPointer {
AddressWriteValue::Pointer(ptr_val),
);
}
zelf.0.keep_alive(index as usize, kept_alive);
return zelf.0.keep_ref(index as usize, value.clone(), vm);
return zelf.0.keep_ref_with_hidden(
index as usize,
value.clone(),
Some(kept_alive),
vm,
);
} else if type_code.as_deref() == Some("Z")
&& let Some(s) = value.downcast_ref::<PyStr>()
{
@@ -612,11 +653,28 @@ impl PyCPointer {
}
return zelf.0.keep_ref(index as usize, holder, vm);
}
Self::write_value_at_address(addr, element_size, &value, type_code.as_deref(), vm)?;
if type_code.as_deref() == Some("O") {
unsafe {
rustpython_host_env::ctypes::write_value_to_address(
addr,
element_size,
AddressWriteValue::Pointer(value.get_id()),
);
}
} else if super::simple::is_swapped(proto_type) {
let type_code = type_code
.as_deref()
.ok_or_else(|| vm.new_type_error("no _type_ attribute"))?;
let bytes = super::simple::swapped_value_bytes(type_code, &value, vm)?;
unsafe {
rustpython_host_env::ctypes::copy_bytes_to_address(addr, &bytes, element_size);
}
} else {
Self::write_value_at_address(addr, element_size, &value, type_code.as_deref(), vm)?;
}
// KeepRef: store reference to keep value alive using actual index
zelf.0.keep_ref(index as usize, value, vm)
}
// KeepRef: store reference to keep value alive using actual index
zelf.0.keep_ref(index as usize, value, vm)
}
/// Read a value from memory address
+66 -65
View File
@@ -1,11 +1,12 @@
use super::_ctypes::CArgObject;
use super::array::PyCArray;
use super::base::{
CArgValue, CDATA_BUFFER_METHODS, PyCData, StgInfo, StgInfoFlags, bytes_to_pyobject,
CArgValue, CDATA_BUFFER_METHODS, ParamFunc, PyCData, StgInfo, StgInfoFlags, bytes_to_pyobject,
};
use super::function::PyCFuncPtr;
use super::pointer::PyCPointer;
use crate::builtins::{PyByteArray, PyBytes, PyInt, PyNone, PyStr, PyType, PyTypeRef};
use crate::class::StaticType;
use crate::convert::ToPyObject;
use crate::function::{Either, FuncArgs, OptionalArg};
use crate::protocol::{BufferDescriptor, PyBuffer, PyNumberMethods, PySequenceMethods};
@@ -569,7 +570,9 @@ impl Initializer for PyCSimpleType {
super::base::set_or_init_stginfo(&type_ref, stg_info);
// Create __ctype_le__ and __ctype_be__ swapped types
create_swapped_types(&type_ref, &type_str, vm)?;
if type_ref.class().is(Self::static_type()) {
create_swapped_types(&type_ref, &type_str, args, vm)?;
}
Ok(())
}
@@ -585,15 +588,9 @@ impl Initializer for PyCSimpleType {
fn create_swapped_types(
type_ref: &Py<PyType>,
type_str: &str,
mut args: FuncArgs,
vm: &VirtualMachine,
) -> PyResult<()> {
use crate::builtins::PyDict;
// Avoid infinite recursion - if __ctype_le__ already exists, skip
if type_ref.as_object().get_attr("__ctype_le__", vm).is_ok() {
return Ok(());
}
// Types that don't support byte order swapping - no __ctype_le__/__ctype_be__
// c_void_p (P), c_char_p (z), c_wchar_p (Z), c_wchar (u), py_object (O)
let unsupported_types = ["P", "z", "Z", "u", "O"];
@@ -617,31 +614,15 @@ fn create_swapped_types(
// Check system byte order at compile time
let is_little_endian = cfg!(target_endian = "little");
// Create dict for the swapped (non-native) type
let swapped_dict: crate::PyRef<crate::builtins::PyDict> = PyDict::default().into_ref(&vm.ctx);
swapped_dict.set_item("_type_", vm.ctx.new_str(type_str).into(), vm)?;
// Create the swapped type using the same metaclass
let metaclass = type_ref.class();
let bases = vm.ctx.new_tuple(vec![type_ref.as_object().to_owned()]);
// Set placeholder first to prevent recursion
set_attrs!(
type_ref.as_object(), vm,
"__ctype_le__" => vm.ctx.none(),
"__ctype_be__" => vm.ctx.none(),
);
// Create only the non-native endian type
let suffix = if is_little_endian { "_be" } else { "_le" };
let swapped_type = metaclass.as_object().call(
(
vm.ctx.new_str(format!("{}{}", type_ref.name(), suffix)),
bases,
swapped_dict.as_object().to_owned(),
),
vm,
)?;
args.args[0] = vm
.ctx
.new_str(format!("{}{}", type_ref.name(), suffix))
.into();
// Endian variants share bases and class attributes; neither subclasses
// the other. Bypass the initializer to avoid generating another pair.
let swapped_type = PyType::slot_new(type_ref.class().to_owned(), args, vm)?;
// Set _swappedbytes_ on the swapped type to indicate byte swapping is needed
set_attrs!(
@@ -650,14 +631,15 @@ fn create_swapped_types(
);
// Update swapped type's StgInfo format to use opposite endian prefix
if let Ok(swapped_type_ref) = swapped_type.clone().downcast::<PyType>()
&& let Some(mut sw_stg) = swapped_type_ref.get_type_data_mut::<StgInfo>()
{
if let Ok(swapped_type_ref) = swapped_type.clone().downcast::<PyType>() {
let mut sw_stg = type_ref.stg_info(vm)?.clone();
// Swapped: little-endian system uses big-endian prefix and vice versa
sw_stg.big_endian = is_little_endian;
sw_stg.format = Some(alloc_format_string_for_type(
type_str.chars().next().unwrap_or('?'),
is_little_endian,
));
super::base::set_or_init_stginfo(&swapped_type_ref, sw_stg);
}
// Set attributes based on system byte order
@@ -704,6 +686,32 @@ impl Debug for PyCSimple {
}
}
pub(super) fn is_swapped(cls: &Py<PyType>) -> bool {
cls.stg_info_opt().is_some_and(|info| {
info.paramfunc == ParamFunc::Simple && info.big_endian != cfg!(target_endian = "big")
})
}
pub(super) fn returns_scalar(cls: &Py<PyType>) -> bool {
cls.base
.deref()
.is_some_and(|base| base.is(PyCSimple::static_type()))
}
pub(super) fn swapped_value_bytes(
type_code: &str,
value: &PyObject,
vm: &VirtualMachine,
) -> PyResult<Vec<u8>> {
// Convert once before writing: numeric conversion may reenter ctypes.
let value = match type_code {
"f" | "d" | "g" => SimpleStorageValue::Float(value.try_float(vm)?.to_f64()),
"?" | "v" => SimpleStorageValue::Bool(value.to_owned().try_to_bool(vm)?),
_ => SimpleStorageValue::Signed(bigint_to_i128_wrapping(value.try_index(vm)?.as_bigint())),
};
Ok(simple_storage_value_to_bytes_endian(type_code, value, true))
}
fn value_to_bytes_endian(
_type_: &str,
value: &PyObject,
@@ -979,7 +987,7 @@ impl Constructor for PyCSimple {
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
let buffer = rustpython_host_env::ctypes::pointer_bytes(ptr);
let cdata = PyCData::from_bytes(buffer, Some(v.clone()));
*cdata.base.write() = Some(kept_alive);
cdata.keep_alive(0, kept_alive);
return Self(cdata).into_ref_with_type(vm, cls).map(Into::into);
}
} else if _type_ == "Z"
@@ -1006,10 +1014,15 @@ impl Constructor for PyCSimple {
}
};
// Check if this is a swapped endian type (presence of attribute indicates swapping)
let swapped = cls.as_object().get_attr("_swappedbytes_", vm).is_ok();
// Storage metadata belongs to this exact type, unlike inherited attributes.
let swapped = is_swapped(&cls);
let buffer = value_to_bytes_endian(&_type_, &value, swapped, vm);
let buffer = if _type_ == "O" && init_arg.is_none() {
// An omitted py_object value is NULL, not the None singleton.
rustpython_host_env::ctypes::pointer_bytes(0)
} else {
value_to_bytes_endian(&_type_, &value, swapped, vm)
};
// For c_char_p (type "z"), c_wchar_p (type "Z"), and py_object (type "O"),
// store the initial value in _objects
@@ -1117,20 +1130,12 @@ impl Py<PyCSimple> {
// O_get: py_object - read PyObject pointer from buffer
if type_code == "O" {
let buffer = zelf.0.buffer.read();
let ptr = rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer);
if ptr == 0 {
return Err(vm.new_value_error("PyObject is NULL"));
}
// Non-NULL: return stored object from _objects if available
if let Some(obj) = zelf.0.objects.read().as_ref() {
return Ok(obj.clone());
}
return Err(vm.new_value_error("PyObject is NULL"));
let ptr = rustpython_host_env::ctypes::read_pointer_from_buffer(&zelf.0.buffer.read());
return zelf.0.object_at(ptr, None, vm);
}
// Check if this is a swapped endian type (presence of attribute indicates swapping)
let swapped = cls.as_object().get_attr("_swappedbytes_", vm).is_ok();
// Storage metadata belongs to this exact type, unlike inherited attributes.
let swapped = is_swapped(cls);
// Read value from buffer, swap bytes if needed
let buffer = zelf.0.buffer.read();
@@ -1166,30 +1171,25 @@ impl Py<PyCSimple> {
if type_code == "z" {
if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
*zelf.0.buffer.write() =
alloc::borrow::Cow::Owned(rustpython_host_env::ctypes::pointer_bytes(ptr));
*zelf.0.objects.write() = Some(value);
*zelf.0.base.write() = Some(kept_alive);
zelf.0
.write_bytes_at_offset(0, &rustpython_host_env::ctypes::pointer_bytes(ptr));
zelf.0.keep_value(value, Some(kept_alive), vm)?;
return Ok(());
}
} else if type_code == "Z"
&& let Some(s) = value.downcast_ref::<PyStr>()
{
let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm);
*zelf.0.buffer.write() =
alloc::borrow::Cow::Owned(rustpython_host_env::ctypes::pointer_bytes(ptr));
*zelf.0.objects.write() = Some(holder);
zelf.0
.write_bytes_at_offset(0, &rustpython_host_env::ctypes::pointer_bytes(ptr));
zelf.0.keep_value(holder, None, vm)?;
return Ok(());
}
let content = set_primitive(&type_code, &value, vm)?;
// Check if this is a swapped endian type (presence of attribute indicates swapping)
let swapped = instance
.class()
.as_object()
.get_attr("_swappedbytes_", vm)
.is_ok();
// Storage metadata belongs to this exact type, unlike inherited attributes.
let swapped = is_swapped(instance.class());
// Update buffer when value changes
let buffer_bytes = value_to_bytes_endian(&type_code, &content, swapped, vm);
@@ -1198,11 +1198,12 @@ impl Py<PyCSimple> {
// Otherwise replace with new owned buffer
let mut buffer = zelf.0.buffer.write();
write_simple_storage_buffer(&mut buffer, &buffer_bytes);
drop(buffer);
// For c_char_p (type "z"), c_wchar_p (type "Z"), and py_object (type "O"),
// keep the reference in _objects
if type_code == "z" || type_code == "Z" || type_code == "O" {
*zelf.0.objects.write() = Some(value);
zelf.0.keep_value(value, None, vm)?;
}
Ok(())
}
+4 -1
View File
@@ -85,7 +85,10 @@ impl Initializer for PyCStructType {
// Process _fields_ if defined directly on this class (not inherited)
if let Some(fields_attr) = new_type.get_direct_attr(vm.ctx.intern_str("_fields_")) {
Self::process_fields(&new_type, &fields_attr, vm)?;
// Let the metaclass transform field types, as for later assignment.
new_type
.as_object()
.set_attr(vm.ctx.intern_str("_fields_"), fields_attr, vm)?;
} else {
// No _fields_ defined - try to copy from base class (PyCStgInfo_clone)
let (has_base_info, base_clone) = {
+251
View File
@@ -4,6 +4,7 @@ import itertools
import os as _os
import sys as _sys
import types as _types
import weakref
from _ctypes import RTLD_GLOBAL, RTLD_LOCAL, Array, _SimpleCData, sizeof
from _ctypes import CFuncPtr as _CFuncPtr
from struct import calcsize as _calcsize
@@ -604,3 +605,253 @@ for read in (
raise AssertionError("non-pointer argument wrapper accepted")
print("done")
# Indexed compound objects are views of the element, not decoded scalars.
class IndexedPoint(ctypes.Structure):
_fields_ = [("x", ctypes.c_int)]
class IndexedInt(ctypes.c_int):
pass
for _element in (IndexedPoint, IndexedInt, ctypes.c_int * 2):
_owner = (_element * 2)()
_view = _owner[1]
assert type(_view) is _element
assert _view._b_base_ is _owner
assert ctypes.addressof(_view) == ctypes.addressof(_owner) + ctypes.sizeof(_element)
_pointer = ctypes.cast(_owner, ctypes.POINTER(_element))
_pointer[1] = _pointer[1]
assert bytes(_pointer[1]) == bytes(_view)
_array = (ctypes.c_int * 2)(1, 2)
_pointer = ctypes.pointer(_array)
for _value, _view in enumerate((_pointer[0], _pointer.contents), 42):
assert _view._b_base_ is _pointer
assert len(_view) == 2
_view[1] = _value
assert _array[1] == _value
_callback_type = ctypes.CFUNCTYPE(ctypes.c_int)
_callbacks = (_callback_type * 1)(_callback_type(lambda: 42))
_callback = _callbacks[0]
assert _callback._b_base_ is _callbacks
_copied_callbacks = (_callback_type * 1)(_callback)
del _callback, _callbacks
gc.collect()
assert _copied_callbacks[0]() == 42
class CallbackField(ctypes.Structure):
_fields_ = [("callback", _callback_type)]
assert CallbackField(_copied_callbacks[0]).callback() == 42
# Nested element keys and copies must retain both pointees after the source dies.
_nested = (((ctypes.POINTER(ctypes.c_int) * 1) * 1) * 2)()
_refs = []
for _index in range(2):
_item = ctypes.c_int(_index)
_refs.append(weakref.ref(_item))
_nested[_index][0][0] = ctypes.pointer(_item)
_copy = type(_nested)(*_nested)
del _nested, _item
gc.collect()
assert [ref().value for ref in _refs] == [0, 1]
# Replacing a pointer view's contents updates its slot and retains the pointee.
_replacement = ctypes.c_int(42)
_replacement_ref = weakref.ref(_replacement)
_view = _copy[1][0][0]
_view.contents = _replacement
del _replacement, _view
gc.collect()
assert _replacement_ref() is not None
assert _copy[1][0][0].contents.value == 42
# Replacing a callback may reenter the same array from its finalizer.
_finalized = []
class ReenteringCallback:
def __call__(self):
return 1
def __del__(self):
_finalized.append(_copied_callbacks[0]())
_copied_callbacks[0] = _callback_type(lambda: 3)
_copied_callbacks[0] = _callback_type(ReenteringCallback())
_copied_callbacks[0] = _callback_type(lambda: 2)
assert _finalized == [2]
assert _copied_callbacks[0]() == 3
# Pointer-valued scalar views retain their owner and write through to it.
for _type, _old, _new in (
(ctypes.c_char_p, b"old", b"new"),
(ctypes.c_wchar_p, "old", "new"),
):
class IndexedStringPointer(_type):
pass
_owner = (IndexedStringPointer * 1)(_old)
_view = _owner[0]
_view.value = _new
assert _view._b_base_ is _owner
assert _owner[0].value == _new
_copy = type(_owner)(_view)
del _owner, _view
gc.collect()
assert _copy[0].value == _new
# Reentry must see matching pointer bytes and owners, including private copies.
class StringField(ctypes.Structure):
_fields_ = [("text", ctypes.c_char_p)]
class ReenteringBytes(bytes):
def __del__(self):
if _replace_record:
_strings[0] = StringField(_inner)
_events.append(_strings[0].text)
else:
_strings.text = _inner
_events.append(_strings.text)
_inner = b"inner replacement" * 20
for _replace_record in (False, True):
_events = []
if _replace_record:
_strings = (StringField * 1)(StringField(ReenteringBytes(b"old")))
_strings[0] = StringField(b"outer replacement")
assert _strings[0].text == _inner
else:
_strings = StringField(ReenteringBytes(b"old"))
_strings.text = b"outer replacement"
assert _strings.text == _inner
assert _events == [_inner]
# py_object views locate the referent in their owner, including after a copy.
class IndexedObject(ctypes.py_object):
pass
_object = {}
_object_scalar = IndexedObject(_object)
_objects = (IndexedObject * 1)(_object_scalar)
assert type(_objects).from_buffer(memoryview(_objects))[0].value is _object
_object_pointer = ctypes.cast(_objects, ctypes.POINTER(IndexedObject))
_object_pointer[0] = _object_scalar
_object_scalar.value = object()
assert _object_pointer[0].value is _object
_copy = type(_objects)(_objects[0])
del _objects, _object_pointer
gc.collect()
assert _copy[0].value is _object
_copy[0].value = None
assert _copy[0].value is None
class RetentionKey(str):
__hash__ = str.__hash__
def __eq__(self, other):
raise AssertionError("py_object lookup called key equality")
_copy[0].value = _object
_copy._objects.clear()
_copy._objects[RetentionKey("0:0")] = _object
assert _copy[0].value is _object
# Generated endian types return scalars, but their user subclasses return views.
_swapped = (
ctypes.c_uint32.__ctype_be__
if _sys.byteorder == "little"
else ctypes.c_uint32.__ctype_le__
)
_words = (_swapped * 1).from_buffer_copy(b"\x01\x02\x03\x04")
_word_pointer = ctypes.cast(_words, ctypes.POINTER(_swapped))
_expected = int.from_bytes(
bytes(_words), "big" if _sys.byteorder == "little" else "little"
)
assert type(_words[0]) is type(_word_pointer[0]) is int
assert _words[0] == _word_pointer[0] == _expected
_words[0] = 0x11223344
assert bytes(_words) == bytes(_swapped(0x11223344))
_word_pointer[0] = 0x55667788
assert bytes(_words) == bytes(_swapped(0x55667788))
class IndexedSwapped(_swapped):
pass
_words = (IndexedSwapped * 1).from_buffer_copy(b"\x01\x02\x03\x04")
assert type(_words[0]) is IndexedSwapped
assert _words[0].value == int.from_bytes(bytes(_words), _sys.byteorder)
# Field assignment copies encoded instances; only numeric inputs are converted.
for _base in (ctypes.Structure, ctypes.Union):
for _field_type in (_swapped, IndexedSwapped):
class EndianField(_base):
_fields_ = [("word", _field_type)]
_value = _field_type(0x01020304)
_aggregate = EndianField(_value)
assert bytes(_aggregate) == bytes(_value)
_field = EndianField.from_buffer_copy(bytes(_value)).word
assert getattr(_field, "value", _field) == _value.value
if _field_type is IndexedSwapped:
_aggregate.word = 0x55667788
assert bytes(_aggregate) == bytes(IndexedSwapped(0x55667788))
# Endian fields reject the native sibling rather than converting its bytes.
assert not issubclass(_swapped, ctypes.c_uint32)
for _base in (
(ctypes.BigEndianStructure, ctypes.BigEndianUnion)
if _sys.byteorder == "little"
else (ctypes.LittleEndianStructure, ctypes.LittleEndianUnion)
):
class ConvertedEndianField(_base):
_fields_ = [("word", ctypes.c_uint32)]
assert ConvertedEndianField._fields_[0][1] is _swapped
try:
ConvertedEndianField(ctypes.c_uint32(0x01020304))
except TypeError:
pass
else:
raise AssertionError("incompatible native field value accepted")
_value = _swapped(0x01020304)
assert bytes(ConvertedEndianField(_value)) == bytes(_value)
# Array assignment cannot complete a pointer field's missing target type.
class IncompletePointer(ctypes._Pointer):
pass
class IncompletePointerField(ctypes.Structure):
_fields_ = [("value", IncompletePointer)]
_field = IncompletePointerField()
_before = bytes(_field)
try:
_field.value = (ctypes.c_int * 1)(42)
except TypeError:
pass
else:
raise AssertionError("array accepted for an incomplete pointer field")
assert bytes(_field) == _before