Fix ctypes assignment conversion and backing-buffer retention (#9251)

* Fix ctypes assignment conversion and backing-buffer retention

Assisted-by: Codex:GPT-6

* Preserve ctypes assignment metadata and pointer-width writes

Assisted-by: Codex:GPT-6

* Enable passing Windows console tests

Assisted-by: Codex:GPT-6
This commit is contained in:
1ndahous3 authored and GitHub committed 2026-10-21 12:06:39 +09:00
1 parent a96c0570a8
commit 11a6455c61
7 files changed
+555 -795

No files matched your search

-11
View File
@@ -480,7 +480,6 @@ class WindowsConsoleGetEventTests(TestCase):
self.assertEqual(self.get_event([ir]), None)
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_enter(self):
ir = self.get_input_record("\r", self.VK_RETURN)
self.assertEqual(self.get_event([ir]), Event("key", "\n"))
@@ -492,13 +491,11 @@ class WindowsConsoleGetEventTests(TestCase):
self.get_event([ir]), Event("key", "backspace"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_m(self):
ir = self.get_input_record("m", self.VK_M)
self.assertEqual(self.get_event([ir]), Event("key", "m"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_M(self):
ir = self.get_input_record("M", self.VK_M, self.SHIFT_PRESSED)
self.assertEqual(self.get_event([ir]), Event("key", "M"))
@@ -554,7 +551,6 @@ class WindowsConsoleGetEventTests(TestCase):
self.assertEqual(self.get_event([ir]), None)
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_m_LEFT_ALT_PRESSED(self):
ir = self.get_input_record(
"m", vcode=self.VK_M, control=self.LEFT_ALT_PRESSED)
@@ -562,7 +558,6 @@ class WindowsConsoleGetEventTests(TestCase):
self.assertEqual(self.console.get_event(), Event("key", "m"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_m_RIGHT_ALT_PRESSED(self):
ir = self.get_input_record(
"m", vcode=self.VK_M, control=self.RIGHT_ALT_PRESSED)
@@ -570,7 +565,6 @@ class WindowsConsoleGetEventTests(TestCase):
self.assertEqual(self.console.get_event(), Event("key", "m"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_AltGr_7(self):
# E.g. on a German keyboard layout, '{' is entered via
# AltGr + 7, where AltGr is the right Alt key on the keyboard.
@@ -587,7 +581,6 @@ class WindowsConsoleGetEventTests(TestCase):
self.assertEqual(self.get_event([ir]), Event("key", "{"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_AltGr_m(self):
# E.g. on a German keyboard layout, this yields 'µ'
# Let's use LEFT_ALT_PRESSED and RIGHT_CTRL_PRESSED this
@@ -597,7 +590,6 @@ class WindowsConsoleGetEventTests(TestCase):
self.assertEqual(self.get_event([ir]), Event("key", "µ"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_umlaut_a_german(self):
ir = self.get_input_record("ä", self.VK_OEM_7)
self.assertEqual(self.get_event([ir]), Event("key", "ä"))
@@ -612,21 +604,18 @@ class WindowsConsoleGetEventTests(TestCase):
# are sufficient to test here, the real tests happen in test_eventqueue
# and test_keymap.
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_enter_vt(self):
ir = self.get_input_record("\r")
self.assertEqual(self.get_event([ir], vt_support=True),
Event("key", "\n"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_backspace_vt(self):
ir = self.get_input_record("\x7f")
self.assertEqual(self.get_event([ir], vt_support=True),
Event("key", "backspace", b"\x7f"))
self.assertEqual(self.mock.call_count, 1)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_up_vt(self):
irs = [self.get_input_record(x) for x in "\x1b[A"]
self.assertEqual(self.get_event(irs, vt_support=True),
+15 -1
View File
@@ -1083,7 +1083,7 @@ pub fn simple_storage_value_to_bytes_endian(
}
_ => vec![0; 2],
},
"P" | "z" | "Z" => match value {
"P" | "z" | "Z" | "X" => match value {
SimpleStorageValue::Pointer(value) => {
uint_to_sized_bytes_endian(value as u64, pointer_size(), swapped)
}
@@ -2994,6 +2994,20 @@ pub fn dlsym_checked(_handle: usize, symbol_name: &CStr) -> Result<*mut c_void,
mod tests {
use super::*;
#[test]
fn bstr_storage_preserves_pointer_width() {
for address in [0, usize::MAX] {
assert_eq!(
simple_storage_value_to_bytes_endian(
"X",
SimpleStorageValue::Pointer(address),
false,
),
address.to_ne_bytes(),
);
}
}
#[test]
fn simple_type_is_pointer_classifies_codes() {
assert!(simple_type_is_pointer("z"));
+23 -243
View File
@@ -17,10 +17,9 @@ use alloc::borrow::Cow;
use num_traits::{Signed, ToPrimitive};
use rustpython_host_env::ctypes::{
ArrayElementWriteValue, DecodedValue, WCHAR_SIZE, WCharArrayWriteError, char_array_field_value,
int_to_sized_bytes, read_array_element, simple_type_size, uint_to_sized_bytes,
wchar_from_bytes, write_array_element, write_char_array_raw, write_char_array_value,
write_wchar_array_value, wstring_from_bytes, zeroed_bytes,
DecodedValue, WCHAR_SIZE, WCharArrayWriteError, char_array_field_value, read_array_element,
simple_type_size, wchar_from_bytes, write_char_array_raw, write_char_array_value,
write_wchar_array_value, wstring_from_bytes,
};
fn cow_bytes_as_mut<'buf>(buffer: &'buf mut Cow<'_, [u8]>) -> &'buf mut [u8] {
@@ -508,33 +507,6 @@ impl PyCArray {
PyGenericAlias::from_args(cls, object, vm)
}
fn int_to_bytes(i: &malachite_bigint::BigInt, size: usize) -> Vec<u8> {
// Try unsigned first (handles values like 0xFFFFFFFF that overflow signed)
// then fall back to signed (handles negative values)
match size {
1 => {
if let Some(v) = i.to_u8() {
vec![v]
} else {
vec![i.to_i8().unwrap_or(0) as u8]
}
}
2 => i.to_u16().map_or_else(
|| int_to_sized_bytes(i.to_i16().unwrap_or(0).into(), 2),
|v| uint_to_sized_bytes(v.into(), 2),
),
4 => i.to_u32().map_or_else(
|| int_to_sized_bytes(i.to_i32().unwrap_or(0).into(), 4),
|v| uint_to_sized_bytes(v.into(), 4),
),
8 => i.to_u64().map_or_else(
|| int_to_sized_bytes(i.to_i64().unwrap_or(0), 8),
|v| uint_to_sized_bytes(v, 8),
),
_ => zeroed_bytes(size),
}
}
fn getitem_by_index(zelf: &Py<Self>, i: isize, vm: &VirtualMachine) -> PyResult {
let stg = zelf.class().stg_info_opt();
let length = stg.as_ref().map_or(0, |i| i.length) as isize;
@@ -616,231 +588,39 @@ impl PyCArray {
}
}
/// Helper to write an element value to a buffer at given offset
fn write_element_to_buffer(
offset: usize,
element_size: usize,
type_code: Option<&str>,
value: &PyObject,
zelf: &Py<Self>,
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(ArrayElementWriteValue::Byte(
b.as_bytes().first().copied().unwrap_or(0),
));
} else if let Ok(int_val) = value.try_int(vm) {
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"));
}
}
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(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);
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")
{
let buffer = simple.0.buffer.read();
(
rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer),
None,
)
} else if let Ok(int_val) = value.try_index(vm) {
(int_val.as_bigint().to_usize().unwrap_or(0), None)
} else {
return Err(vm.new_type_error(format!(
"bytes or integer address expected instead of {} instance",
value.class().name()
)));
};
write(ArrayElementWriteValue::Pointer {
value: ptr_val,
size: element_size,
});
if let Some(c) = converted {
return zelf.0.keep_ref_with_hidden(index, c, hidden, vm);
}
}
Some("Z") => {
let (ptr_val, converted) = if value.is(&vm.ctx.none) {
(0usize, None)
} else if let Some(s) = value.downcast_ref::<PyStr>() {
let (holder, ptr) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm);
(ptr, Some(holder))
} else if let Some(simple) = value.downcast_ref::<super::PyCSimple>()
&& value.class().type_code(vm).as_deref() == Some("Z")
{
let buffer = simple.0.buffer.read();
(
rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer),
None,
)
} else if let Ok(int_val) = value.try_index(vm) {
(int_val.as_bigint().to_usize().unwrap_or(0), None)
} else {
return Err(vm.new_type_error("unicode string or integer address expected"));
};
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) {
float_val.to_f64() as f32
} else if let Ok(int_val) = value.try_int(vm) {
int_val.as_bigint().to_f64().unwrap_or(0.0) as f32
} else {
return Err(vm.new_type_error("a float is required"));
};
write(ArrayElementWriteValue::Float {
value: f32_val.into(),
size: 4,
});
}
Some("d" | "g") => {
// c_double / c_longdouble: convert int/float to f64 bytes
let f64_val = if let Ok(float_val) = value.try_float(vm) {
float_val.to_f64()
} else if let Ok(int_val) = value.try_int(vm) {
int_val.as_bigint().to_f64().unwrap_or(0.0)
} else {
return Err(vm.new_type_error("a float is required"));
};
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().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(ArrayElementWriteValue::Bytes {
bytes: &bytes,
size: element_size,
});
} else {
return Err(vm.new_type_error(format!(
"expected {} instance, not {}",
type_code.unwrap_or("value"),
value.class().name()
)));
}
}
}
// KeepRef
if super::base::PyCData::should_keep_ref(value) {
zelf.0.keep_cdata(index, value, vm)?;
}
Ok(())
}
fn setitem_by_index(
zelf: &Py<Self>,
i: isize,
value: &PyObject,
vm: &VirtualMachine,
) -> PyResult<()> {
let stg = zelf.class().stg_info_opt();
let length = stg.as_ref().map_or(0, |i| i.length) as isize;
let (length, element_size, element_type) = {
let stg = zelf.class().stg_info(vm)?;
(
stg.length as isize,
stg.element_size,
stg.element_type
.clone()
.ok_or_else(|| vm.new_type_error("no element type"))?,
)
};
let index = if i < 0 { length + i } else { i };
if index < 0 || index >= length {
return Err(vm.new_index_error("invalid index"));
}
let index = index as usize;
let element_size = stg.as_ref().map_or(0, |i| i.element_size);
let offset = index * element_size;
let type_code = Self::get_element_type_code(zelf, vm);
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()
{
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(());
}
let prepared = super::base::CDataValue::new(
&element_type,
value,
element_size,
super::simple::is_swapped(&element_type),
vm,
)?;
// 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,
)
zelf.0
.write_bytes_at_offset(index * element_size, &prepared.bytes);
prepared.keep(&zelf.0, index, vm)
}
// Array_subscript
+272 -354
View File
@@ -85,6 +85,8 @@ pub struct StgInfo {
pub length: usize, // number of fields (for arrays/structures)
pub proto: Option<PyTypeRef>, // Only for Pointer/ArrayObject
pub flags: StgInfoFlags, // type flags (TYPEFLAG_*, DICTFLAG_*)
// Scalar assignment uses the converter selected at type initialization.
pub(super) scalar_code: Option<char>,
// Array-specific fields
pub element_type: Option<PyTypeRef>, // _type_ for arrays
@@ -123,6 +125,7 @@ impl core::fmt::Debug for StgInfo {
.field("length", &self.length)
.field("proto", &self.proto)
.field("flags", &self.flags)
.field("scalar_code", &self.scalar_code)
.field("element_type", &self.element_type)
.field("element_size", &self.element_size)
.field("format", &self.format)
@@ -143,6 +146,7 @@ impl Default for StgInfo {
length: 0,
proto: None,
flags: StgInfoFlags::empty(),
scalar_code: None,
element_type: None,
element_size: 0,
format: None,
@@ -164,6 +168,7 @@ impl StgInfo {
length: 0,
proto: None,
flags: StgInfoFlags::empty(),
scalar_code: None,
element_type: None,
element_size: 0,
format: None,
@@ -216,6 +221,7 @@ impl StgInfo {
length,
proto: None,
flags,
scalar_code: None,
element_type: Some(element_type),
element_size,
format,
@@ -437,7 +443,43 @@ pub struct PyCData {
/// Used for null-terminated c_char_p buffer copies, since
/// RustPython's PyBytes lacks CPython's internal trailing null.
/// Keyed by unique_key (hierarchical) so nested fields don't collide.
pub(super) kept_refs: PyRwLock<std::collections::HashMap<String, PyObjectRef>>,
kept_refs: PyRwLock<Option<Box<PrivateRefs>>>,
}
#[derive(Debug, Default)]
struct PrivateRefs {
slots: std::collections::HashMap<String, Vec<usize>, rapidhash::quality::RandomState>,
owners: std::collections::HashMap<usize, (PyObjectRef, usize), rapidhash::quality::RandomState>,
}
impl PrivateRefs {
fn replace(&mut self, key: String, owners: Vec<PyObjectRef>) -> Vec<PyObjectRef> {
let old = if owners.is_empty() {
self.slots.remove(&key)
} else {
self.slots
.insert(key, owners.iter().map(|obj| obj.get_id()).collect())
};
// Count each slot's references, but retain each Python owner only once.
// Add first so replacing an owner with itself cannot release it.
for obj in owners {
self.owners.entry(obj.get_id()).or_insert((obj, 0)).1 += 1;
}
let mut displaced = Vec::new();
for id in old.into_iter().flatten() {
let owner = self.owners.get_mut(&id).expect("registered private owner");
owner.1 -= 1;
if owner.1 == 0 {
displaced.push(self.owners.remove(&id).unwrap().0);
}
}
// The caller releases these after unlocking and publishing _objects.
displaced
}
fn snapshot(&self) -> Vec<PyObjectRef> {
self.owners.values().map(|(obj, _)| obj.clone()).collect()
}
}
// Retention metadata is user-mutable. Its fast lookup must not call Python
@@ -470,6 +512,178 @@ impl DictKey for ObjectKey<'_> {
}
}
// Prepare conversions and ownership before writing. Python conversion hooks
// and old-owner finalizers must never run with a storage or metadata lock held.
pub(super) enum CDataBytes {
Integer { bytes: [u8; 8], size: usize },
Owned(Vec<u8>),
}
impl From<Vec<u8>> for CDataBytes {
fn from(bytes: Vec<u8>) -> Self {
Self::Owned(bytes)
}
}
impl core::ops::Deref for CDataBytes {
type Target = [u8];
fn deref(&self) -> &[u8] {
match self {
Self::Integer { bytes, size } => &bytes[..*size],
Self::Owned(bytes) => bytes,
}
}
}
pub(super) struct CDataValue {
pub bytes: CDataBytes,
keep: Option<PyObjectRef>,
hidden: Vec<PyObjectRef>,
temporary: Option<PyObjectRef>,
}
impl CDataValue {
pub(super) fn new(
proto: &Py<PyType>,
value: &PyObject,
size: usize,
swapped: bool,
vm: &VirtualMachine,
) -> PyResult<Self> {
if let Some(source) = value.downcast_ref::<PyCData>() {
if value.fast_isinstance(proto) {
// Snapshot before writing: source and destination may overlap.
let bytes =
rustpython_host_env::ctypes::copy_to_sized_bytes(&source.buffer.read(), size);
let hidden = PyCData::hidden_refs(value);
let keep = PyCData::get_kept_objects(value, vm);
return Ok(Self {
bytes: bytes.into(),
keep: (!vm.is_none(&keep)).then_some(keep),
hidden,
temporary: None,
});
}
if proto
.class()
.fast_issubclass(super::pointer::PyCPointerType::static_type())
&& let Some(array) = value.downcast_ref::<super::array::PyCArray>()
&& {
let target = proto.stg_info(vm)?;
let array_info = array.class().stg_info(vm)?;
target
.proto
.as_deref()
.zip(array_info.element_type.as_deref())
.is_some_and(|(target, element)| target.is(element))
}
{
let address = array.0.buffer.read().as_ptr() as usize;
return Ok(Self {
bytes: rustpython_host_env::ctypes::pointer_to_sized_bytes(address, size)
.into(),
keep: Some(
vm.ctx
.new_tuple(vec![PyCData::get_kept_objects(value, vm), value.to_owned()])
.into(),
),
hidden: Vec::new(),
temporary: None,
});
}
return Err(vm.new_type_error(format!(
"incompatible types, {} instance instead of {} instance",
value.class().name(),
proto.name()
)));
}
let metaclass = proto.class();
let simple_metaclass = super::simple::PyCSimpleType::static_type();
if metaclass.is(simple_metaclass) || metaclass.fast_issubclass(simple_metaclass) {
// Reading the mutable _type_ here could select a wider converter
// than the allocated slot, or change conversion during callbacks.
let code = proto
.stg_info(vm)?
.scalar_code
.ok_or_else(|| vm.new_type_error("abstract class"))?;
let mut encoded = [0; 4];
let type_code = code.encode_utf8(&mut encoded);
let (bytes, keep, hidden) =
super::simple::assignment_value(type_code, value, swapped, vm)?;
return Ok(Self {
bytes,
keep,
hidden: hidden.into_iter().collect(),
temporary: None,
});
}
if let Some(tuple) = value.downcast_ref::<PyTuple>() {
let instance = proto
.as_object()
.call(tuple.as_slice().to_vec(), vm)
.map_err(|e| {
let args = e.args();
let message = args
.as_slice()
.first()
.and_then(|arg| arg.downcast_ref::<PyStr>())
.map(|s| s.to_string())
.unwrap_or_default();
vm.new_runtime_error(format!(
"({}) {}: {message}",
proto.name(),
e.class().name()
))
})?;
let mut prepared = Self::new(proto, &instance, size, swapped, vm)?;
prepared.temporary = Some(instance);
return Ok(prepared);
}
if vm.is_none(value)
&& proto
.class()
.fast_issubclass(super::pointer::PyCPointerType::static_type())
{
return Ok(Self {
bytes: vec![0; size].into(),
keep: None,
hidden: Vec::new(),
temporary: None,
});
}
Err(vm.new_type_error(format!(
"expected {} instance, not {}",
proto.name(),
value.class().name()
)))
}
#[inline]
pub(super) fn keep(
self,
destination: &PyCData,
index: usize,
vm: &VirtualMachine,
) -> PyResult<()> {
// Tuple conversion can create an instance with a finalizer. Keep it
// until the bytes and all owners have been published.
let _temporary = self.temporary;
// Like KeepRef(None), preserve previous owners when assigning a raw
// address: it can still point into their private backing allocation.
if self.keep.is_none() && self.hidden.is_empty() {
return Ok(());
}
// Publish every owner before dropping the displaced value, whose
// finalizer may assign this same slot again.
let _old_hidden = destination.replace_hidden(index, self.hidden);
if let Some(keep) = self.keep {
destination.keep_ref(index, keep, vm)?;
}
Ok(())
}
}
impl PyCData {
/// Create from StgInfo (PyCData_MallocBuffer pattern)
pub(crate) fn from_stg_info(stg_info: &StgInfo) -> Self {
@@ -480,7 +694,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(None),
length: AtomicCell::new(stg_info.length),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
kept_refs: PyRwLock::new(None),
}
}
@@ -493,7 +707,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(objects),
length: AtomicCell::new(0),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
kept_refs: PyRwLock::new(None),
}
}
@@ -510,7 +724,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(objects),
length: AtomicCell::new(length),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
kept_refs: PyRwLock::new(None),
}
}
@@ -531,7 +745,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(None),
length: AtomicCell::new(0),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
kept_refs: PyRwLock::new(None),
}
}
@@ -560,7 +774,7 @@ impl PyCData {
index: AtomicCell::new(idx),
objects: PyRwLock::new(None),
length: AtomicCell::new(0),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
kept_refs: PyRwLock::new(None),
}
}
@@ -595,7 +809,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(Some(objects_dict.into())),
length: AtomicCell::new(length),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
kept_refs: PyRwLock::new(None),
}
}
@@ -813,14 +1027,12 @@ impl PyCData {
}
/// Keep a reference alive without exposing it in _objects.
/// Walks up to root object (same as keep_ref) so the reference
/// 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)));
/// Retains a backing allocation used by a scalar's pointer.
pub fn keep_alive(&self, obj: PyObjectRef) {
drop(self.replace_hidden(0, vec![obj]));
}
fn replace_hidden(&self, index: usize, obj: Option<PyObjectRef>) -> Option<PyObjectRef> {
fn replace_hidden(&self, index: usize, owners: Vec<PyObjectRef>) -> Vec<PyObjectRef> {
let key = self.unique_key(index);
let base = self.base.read().clone();
let root = base.as_ref().map(|base| Self::find_root_object(base));
@@ -829,24 +1041,11 @@ impl PyCData {
.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),
if refs.is_none() && owners.is_empty() {
return Vec::new();
}
}
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)
refs.get_or_insert_with(Default::default)
.replace(key, owners)
}
/// Find the root object (one without a base) by walking up the base chain
@@ -906,30 +1105,34 @@ impl PyCData {
Ok(())
}
/// Get kept objects from a CData instance
/// Returns the _objects of the root CData, or an empty dict if None.
/// Get kept objects from a CData instance, initializing its root container.
pub fn get_kept_objects(value: &PyObject, vm: &VirtualMachine) -> PyObjectRef {
Self::find_root_object(value)
.downcast_ref::<Self>()
.and_then(|cdata| cdata.objects.read().clone())
.unwrap_or_else(|| vm.ctx.new_dict().into())
let root = Self::find_root_object(value);
let Some(cdata) = root.downcast_ref::<Self>() else {
return vm.ctx.none();
};
cdata
.objects
.write()
.get_or_insert_with(|| {
if cdata.length.load() == 0 {
vm.ctx.none()
} else {
vm.ctx.new_dict().into()
}
})
.clone()
}
// 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<()> {
// backing allocations and aliases through other fields or pointers.
// Keep the snapshot flat and deduplicate owners: self-copy must not nest
// a new holder around all previous snapshots.
fn hidden_refs(value: &PyObject) -> Vec<PyObjectRef> {
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)
root.downcast_ref::<Self>()
.and_then(|cdata| cdata.kept_refs.read().as_ref().map(|refs| refs.snapshot()))
.unwrap_or_default()
}
// A scalar owns one reference; a scalar view stores it in its root instead.
@@ -939,7 +1142,7 @@ impl PyCData {
hidden: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
let _old_hidden = self.replace_hidden(0, hidden);
let _old_hidden = self.replace_hidden(0, hidden.into_iter().collect());
if self.base.read().is_some() {
self.keep_ref(0, value, vm)
} else {
@@ -998,7 +1201,9 @@ impl PyCData {
// 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());
if let Some(refs) = self.kept_refs.read().as_ref() {
pending.extend(refs.snapshot());
}
let mut visited = std::collections::HashSet::new();
while let Some(obj) = pending.pop() {
if obj.get_id() == address {
@@ -1010,7 +1215,9 @@ impl PyCData {
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());
if let Some(refs) = cdata.kept_refs.read().as_ref() {
pending.extend(refs.snapshot());
}
} else if let Some(dict) = obj.downcast_ref::<PyDict>() {
pending.extend(dict.values_vec());
} else if let Some(tuple) = obj.downcast_ref::<PyTuple>() {
@@ -1022,12 +1229,6 @@ impl PyCData {
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 {
value.downcast_ref::<Self>().is_some() || value.downcast_ref::<PyBytes>().is_some()
}
/// PyCData_set
/// Sets a field value at the given offset, handling type conversion and KeepRef
#[allow(clippy::too_many_arguments)]
@@ -1073,134 +1274,12 @@ 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();
array_buffer.as_ptr() as usize
};
let addr_bytes = rustpython_host_env::ctypes::pointer_to_sized_bytes(buffer_addr, size);
self.write_bytes_at_offset(offset, &addr_bytes);
self.keep_ref(index, value, vm)?;
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>()
&& let Some(stg) = proto_type.stg_info_opt()
&& stg.element_type.is_some()
{
let items: Option<Vec<PyObjectRef>> =
if let Some(tuple) = value.downcast_ref::<PyTuple>() {
Some(tuple.as_slice().to_vec())
} else {
value
.downcast_ref::<crate::builtins::PyList>()
.map(|list| list.borrow_vec().to_vec())
};
if let Some(items) = items {
let array_obj = proto_type.as_object().call(items, vm).map_err(|e| {
// Wrap errors in RuntimeError with type name prefix
let type_name = proto_type.name().to_string();
let exc_name = e.class().name().to_string();
let exc_args = e.args();
let exc_msg = exc_args
.as_slice()
.first()
.and_then(|a| a.downcast_ref::<PyStr>().map(|s| s.to_string()))
.unwrap_or_default();
vm.new_runtime_error(format!("({type_name}) {exc_name}: {exc_msg}"))
})?;
if let Some(arr) = array_obj.downcast_ref::<super::array::PyCArray>() {
let arr_buffer = arr.0.buffer.read();
let len = core::cmp::min(arr_buffer.len(), size);
self.write_bytes_at_offset(offset, &arr_buffer[..len]);
drop(arr_buffer);
self.keep_ref(index, array_obj, vm)?;
return Ok(());
}
}
}
// Get field type code for special handling
let field_type_code = proto
.get_attr("_type_", vm)
.ok()
.and_then(|attr| attr.downcast_ref::<PyStr>().map(|s| s.to_string()));
// c_char_p (z type) with bytes: store original in _objects, keep
// null-terminated copy alive separately for the pointer.
if field_type_code.as_deref() == Some("z")
&& let Some(bytes_val) = value.downcast_ref::<PyBytes>()
{
let (kept_alive, ptr) = ensure_z_null_terminated(bytes_val, vm);
let result =
rustpython_host_env::ctypes::pointer_to_sized_bytes_endian(ptr, size, needs_swap);
self.write_bytes_at_offset(offset, &result);
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 {
PyCField::value_to_bytes_for_type(type_code, &value, size, vm)?
} else {
(PyCField::value_to_bytes(&value, size, vm), None)
};
// Swap bytes for opposite endianness
if needs_swap {
bytes.reverse();
}
self.write_bytes_at_offset(offset, &bytes);
// KeepRef: for z/Z types use converted value, otherwise use original
if let Some(converted) = converted_value {
self.keep_ref(index, converted, vm)?;
} else if Self::should_keep_ref(&value) {
self.keep_cdata(index, &value, vm)?;
}
Ok(())
let proto_type = proto
.downcast_ref::<PyType>()
.ok_or_else(|| vm.new_type_error("field proto is not a type"))?;
let prepared = CDataValue::new(proto_type, &value, size, needs_swap, vm)?;
self.write_bytes_at_offset(offset, &prepared.bytes);
prepared.keep(self, index, vm)
}
pub(super) fn into_view(self, cls: PyTypeRef, vm: &VirtualMachine) -> PyResult {
@@ -1287,7 +1366,11 @@ impl PyCData {
let proto_metaclass = proto_type.class();
// Simple types: return primitive value
if proto_metaclass.fast_issubclass(super::simple::PyCSimpleType::static_type()) {
if super::simple::returns_scalar(&proto_type) {
if proto_type.type_code(vm).as_deref() == Some("O") {
let address = rustpython_host_env::ctypes::read_pointer_from_buffer(&buffer_data);
return self.object_at(address, Some(index), vm);
}
// Check for byte swapping
let needs_swap = super::simple::is_swapped(&proto_type);
@@ -1306,7 +1389,8 @@ impl PyCData {
let ptr = self.buffer.read().as_ptr().wrapping_add(offset) as *mut u8;
let cdata_obj = unsafe { Self::from_base_obj(ptr, size, base_obj, index) };
if proto_metaclass.fast_issubclass(super::structure::PyCStructType::static_type())
if proto_metaclass.fast_issubclass(super::simple::PyCSimpleType::static_type())
|| 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())
@@ -1697,172 +1781,6 @@ impl GetDescriptor for PyCField {
}
impl PyCField {
/// Convert a Python value to bytes
fn value_to_bytes(value: &PyObject, size: usize, vm: &VirtualMachine) -> Vec<u8> {
// 1. Handle bytes objects
if let Some(bytes) = value.downcast_ref::<PyBytes>() {
rustpython_host_env::ctypes::copy_to_sized_bytes(bytes.as_bytes(), size)
}
// 2. Handle ctypes array instances (copy their buffer)
else if let Some(cdata) = value.downcast_ref::<super::PyCData>() {
let buffer = cdata.buffer.read();
rustpython_host_env::ctypes::copy_to_sized_bytes(&buffer, size)
}
// 4. Handle float values (check before int, since float.try_int would truncate)
else if let Some(float_val) = value.downcast_ref::<crate::builtins::PyFloat>() {
let f = float_val.to_f64();
match size {
4 | 8 => rustpython_host_env::ctypes::float_to_sized_bytes(f, size)
.expect("float size checked"),
_ => unreachable!("wrong payload size"),
}
}
// 4. Handle integer values
else if let Ok(int_val) = value.try_int(vm) {
let i = int_val.as_bigint();
match size {
1 => rustpython_host_env::ctypes::int_to_sized_bytes(
i.to_i8().unwrap_or(0).into(),
size,
),
2 => rustpython_host_env::ctypes::int_to_sized_bytes(
i.to_i16().unwrap_or(0).into(),
size,
),
4 => rustpython_host_env::ctypes::int_to_sized_bytes(
i.to_i32().unwrap_or(0).into(),
size,
),
8 => rustpython_host_env::ctypes::int_to_sized_bytes(i.to_i64().unwrap_or(0), size),
_ => rustpython_host_env::ctypes::zeroed_bytes(size),
}
} else {
rustpython_host_env::ctypes::zeroed_bytes(size)
}
}
/// Convert a Python value to bytes with type-specific handling for pointer types.
/// Returns (bytes, optional holder for wchar buffer).
fn value_to_bytes_for_type(
type_code: &str,
value: &PyObject,
size: usize,
vm: &VirtualMachine,
) -> PyResult<(Vec<u8>, Option<PyObjectRef>)> {
match type_code {
// c_float: always convert to float first (f_set)
"f" => {
let f = if let Some(float_val) = value.downcast_ref::<crate::builtins::PyFloat>() {
float_val.to_f64()
} else if let Ok(int_val) = value.try_int(vm) {
int_val.as_bigint().to_i64().unwrap_or(0) as f64
} else {
return Err(vm.new_type_error(format!(
"float expected instead of {}",
value.class().name()
)));
};
Ok((
rustpython_host_env::ctypes::float_to_sized_bytes(f, 4)
.expect("c_float size is fixed"),
None,
))
}
// c_double: always convert to float first (d_set)
"d" => {
let f = if let Some(float_val) = value.downcast_ref::<crate::builtins::PyFloat>() {
float_val.to_f64()
} else if let Ok(int_val) = value.try_int(vm) {
int_val.as_bigint().to_i64().unwrap_or(0) as f64
} else {
return Err(vm.new_type_error(format!(
"float expected instead of {}",
value.class().name()
)));
};
Ok((
rustpython_host_env::ctypes::float_to_sized_bytes(f, 8)
.expect("c_double size is fixed"),
None,
))
}
// c_longdouble: convert to float (treated as f64 in RustPython)
"g" => {
let f = if let Some(float_val) = value.downcast_ref::<crate::builtins::PyFloat>() {
float_val.to_f64()
} else if let Ok(int_val) = value.try_int(vm) {
int_val.as_bigint().to_i64().unwrap_or(0) as f64
} else {
return Err(vm.new_type_error(format!(
"float expected instead of {}",
value.class().name()
)));
};
Ok((
rustpython_host_env::ctypes::float_to_sized_bytes(f, 8)
.expect("c_longdouble bytes are stored as f64"),
None,
))
}
"z" => {
// c_char_p with bytes is handled in set_field before this call.
// This handles integer address and None cases.
// Integer address
if let Ok(int_val) = value.try_index(vm) {
let v = int_val.as_bigint().to_usize().unwrap_or(0);
return Ok((
rustpython_host_env::ctypes::pointer_to_sized_bytes(v, size),
None,
));
}
// None -> NULL pointer
if vm.is_none(value) {
return Ok((rustpython_host_env::ctypes::zeroed_bytes(size), None));
}
Ok((Self::value_to_bytes(value, size, vm), None))
}
"Z" => {
// c_wchar_p: store pointer to null-terminated wchar_t buffer
if let Some(s) = value.downcast_ref::<PyStr>() {
let (holder, ptr) = str_to_wchar_bytes(s.as_wtf8(), vm);
return Ok((
rustpython_host_env::ctypes::pointer_to_sized_bytes(ptr, size),
Some(holder),
));
}
// Integer address
if let Ok(int_val) = value.try_index(vm) {
let v = int_val.as_bigint().to_usize().unwrap_or(0);
return Ok((
rustpython_host_env::ctypes::pointer_to_sized_bytes(v, size),
None,
));
}
// None -> NULL pointer
if vm.is_none(value) {
return Ok((rustpython_host_env::ctypes::zeroed_bytes(size), None));
}
Ok((Self::value_to_bytes(value, size, vm), None))
}
"P" => {
// c_void_p: store integer as pointer
if let Ok(int_val) = value.try_index(vm) {
let v = int_val.as_bigint().to_usize().unwrap_or(0);
return Ok((
rustpython_host_env::ctypes::pointer_to_sized_bytes(v, size),
None,
));
}
// None -> NULL pointer
if vm.is_none(value) {
return Ok((rustpython_host_env::ctypes::zeroed_bytes(size), None));
}
Ok((Self::value_to_bytes(value, size, vm), None))
}
_ => Ok((Self::value_to_bytes(value, size, vm), None)),
}
}
/// Check if the field type is a c_char array (element type has _type_ == 'c')
fn is_char_array(proto: &PyObject, vm: &VirtualMachine) -> bool {
// Get element_type from StgInfo (for array types)
+17 -176
View File
@@ -1,5 +1,5 @@
use super::base::CDATA_BUFFER_METHODS;
use super::{PyCArray, PyCData, PyCSimple, PyCStructure, StgInfo, StgInfoFlags};
use super::{PyCData, StgInfo, StgInfoFlags};
use crate::atomic_func;
use crate::protocol::{
BufferDescriptor, PyBuffer, PyMappingMethods, PyNumberMethods, PySequenceMethods,
@@ -7,14 +7,13 @@ use crate::protocol::{
use crate::types::{AsBuffer, AsMapping, AsNumber, AsSequence, Constructor, Initializer};
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine,
builtins::{PyBytes, PyInt, PyList, PySlice, PyStr, PyType, PyTypeRef},
class::StaticType,
builtins::{PyInt, PyList, PySlice, PyStr, PyType, PyTypeRef},
function::{FuncArgs, OptionalArg},
};
use alloc::borrow::Cow;
use num_traits::ToPrimitive;
use rustpython_host_env::ctypes::{
AddressValue, AddressWriteValue, IntegerValue, pointer_item_address, read_pointer_char_slice,
AddressValue, IntegerValue, pointer_item_address, read_pointer_char_slice,
read_pointer_wchar_slice,
};
@@ -577,104 +576,22 @@ impl PyCPointer {
return Err(vm.new_value_error("NULL pointer access"));
}
// Get element type, size and type_code from StgInfo.proto
let stg_info = zelf.class().stg_info(vm)?;
let proto_type = stg_info.proto();
// Get type code from proto's _type_ attribute
let type_code: Option<String> = proto_type
.as_object()
.get_attr("_type_", vm)
.ok()
.and_then(|t| t.downcast_ref::<PyStr>().map(|s| s.to_string()));
let element_size = proto_type
.stg_info_opt()
.map_or_else(rustpython_host_env::ctypes::pointer_size, |info| info.size);
// Calculate address
let proto_type = zelf.class().stg_info(vm)?.proto().to_owned();
let element_size = proto_type.stg_info(vm)?.size;
let prepared = super::base::CDataValue::new(
&proto_type,
&value,
element_size,
super::simple::is_swapped(&proto_type),
vm,
)?;
let addr = pointer_item_address(ptr_value, index, element_size);
// 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()))
{
// 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, &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")
&& let Some(bytes) = value.downcast_ref::<PyBytes>()
{
let (kept_alive, ptr_val) = super::base::ensure_z_null_terminated(bytes, vm);
unsafe {
rustpython_host_env::ctypes::write_value_to_address(
addr,
element_size,
AddressWriteValue::Pointer(ptr_val),
);
}
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>()
{
let (holder, ptr_val) = super::base::str_to_wchar_bytes(s.as_wtf8(), vm);
unsafe {
rustpython_host_env::ctypes::write_value_to_address(
addr,
element_size,
AddressWriteValue::Pointer(ptr_val),
);
}
return zelf.0.keep_ref(index as usize, holder, 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)
// SAFETY: pointer indexing has the same caller-provided valid-storage
// requirement as reading the element. The source snapshot cannot overlap it.
unsafe {
rustpython_host_env::ctypes::copy_bytes_to_address(addr, &prepared.bytes, element_size);
}
prepared.keep(&zelf.0, index as usize, vm)
}
/// Read a value from memory address
@@ -693,82 +610,6 @@ impl PyCPointer {
AddressValue::Bytes(bytes) => vm.ctx.new_bytes(bytes).into(),
}
}
/// Write a value to memory address
fn write_value_at_address(
addr: usize,
size: usize,
value: &PyObject,
type_code: Option<&str>,
vm: &VirtualMachine,
) -> PyResult<()> {
unsafe {
// Handle c_char_p (z) and c_wchar_p (Z) - store pointer address
// Note: PyBytes/PyStr cases are handled by caller (setitem_by_index)
if let Some("z" | "Z") = type_code {
let ptr_val = if vm.is_none(value) {
0usize
} else if let Ok(int_val) = value.try_index(vm) {
super::simple::bigint_to_i128_wrapping(int_val.as_bigint()) as usize
} else {
return Err(vm.new_type_error("bytes/string or integer address expected"));
};
rustpython_host_env::ctypes::write_value_to_address(
addr,
size,
AddressWriteValue::Pointer(ptr_val),
);
return Ok(());
}
// Try to get value as integer
// Use write_unaligned for safety on strict-alignment architectures
if let Ok(int_val) = value.try_int(vm) {
let i = int_val.as_bigint();
let wrapped = super::simple::bigint_to_i128_wrapping(i);
let bytes;
let write_value = match size {
1 => AddressWriteValue::U8(wrapped as u8),
2 => AddressWriteValue::I16(wrapped as i16),
4 => AddressWriteValue::I32(wrapped as i32),
8 => AddressWriteValue::I64(wrapped as i64),
_ => {
bytes = i.to_signed_bytes_le();
AddressWriteValue::Bytes(&bytes)
}
};
rustpython_host_env::ctypes::write_value_to_address(addr, size, write_value);
return Ok(());
}
// Try to get value as float
if let Ok(float_val) = value.try_float(vm) {
let f = float_val.to_f64();
rustpython_host_env::ctypes::write_value_to_address(
addr,
size,
AddressWriteValue::Float(f),
);
return Ok(());
}
// Try bytes
if value.check_buffer() {
let bytes = value.try_bytes_like(vm, |b| b.to_vec())?;
rustpython_host_env::ctypes::write_value_to_address(
addr,
size,
AddressWriteValue::Bytes(&bytes),
);
return Ok(());
}
Err(vm.new_type_error(format!(
"cannot convert {} to ctypes data",
value.class().name()
)))
}
}
}
impl AsNumber for PyCPointer {
+105 -10
View File
@@ -1,7 +1,8 @@
use super::_ctypes::CArgObject;
use super::array::PyCArray;
use super::base::{
CArgValue, CDATA_BUFFER_METHODS, ParamFunc, PyCData, StgInfo, StgInfoFlags, bytes_to_pyobject,
CArgValue, CDATA_BUFFER_METHODS, CDataBytes, ParamFunc, PyCData, StgInfo, StgInfoFlags,
bytes_to_pyobject,
};
use super::function::PyCFuncPtr;
use super::pointer::PyCPointer;
@@ -75,6 +76,9 @@ fn new_simple_type(
}
pub(super) fn bigint_to_i128_wrapping(value: &malachite_bigint::BigInt) -> i128 {
if let Some(value) = value.to_i128() {
return value;
}
let bytes = value.to_signed_bytes_le();
let fill = bytes
.last()
@@ -553,6 +557,7 @@ impl Initializer for PyCSimpleType {
let size = simple_type_size(&type_str).expect("invalid ctypes simple type");
let align = simple_type_align(&type_str).expect("invalid ctypes simple type");
let mut stg_info = StgInfo::new(size, align);
stg_info.scalar_code = type_str.chars().next();
// Set format for PEP 3118 buffer protocol
stg_info.format = Some(alloc_format_string_for_type(
@@ -698,18 +703,108 @@ pub(super) fn returns_scalar(cls: &Py<PyType>) -> bool {
.is_some_and(|base| base.is(PyCSimple::static_type()))
}
pub(super) fn swapped_value_bytes(
pub(super) fn assignment_value(
type_code: &str,
value: &PyObject,
swapped: bool,
vm: &VirtualMachine,
) -> PyResult<Vec<u8>> {
) -> PyResult<(CDataBytes, Option<PyObjectRef>, Option<PyObjectRef>)> {
let mut keep = None;
let mut hidden = None;
// Convert once before writing: numeric conversion may reenter ctypes.
let value = match type_code {
let storage = match type_code {
"c" => {
let byte = if let Some(bytes) = value.downcast_ref::<PyBytes>() {
match bytes.as_bytes() {
[byte] => *byte,
_ => return Err(vm.new_type_error("one character bytes expected")),
}
} else if let Some(bytes) = value.downcast_ref::<PyByteArray>() {
match &*bytes.borrow_buf() {
[byte] => *byte,
_ => return Err(vm.new_type_error("one character bytearray expected")),
}
} else {
value
.downcast_ref::<PyInt>()
.and_then(|integer| integer.as_bigint().to_u8())
.ok_or_else(|| {
vm.new_type_error("one character bytes, bytearray or integer expected")
})?
};
SimpleStorageValue::Byte(byte)
}
"u" => {
let text = value
.downcast_ref::<PyStr>()
.ok_or_else(|| vm.new_type_error("unicode string expected"))?;
let mut chars = text.as_wtf8().code_points();
let character = chars
.next()
.filter(|_| chars.next().is_none())
.ok_or_else(|| vm.new_type_error("one character unicode string expected"))?;
if rustpython_host_env::ctypes::WCHAR_SIZE == 2 && character.to_u32() > 0xffff {
return Err(vm.new_type_error("a single UTF-16 code unit is required"));
}
SimpleStorageValue::Wchar(character.to_u32())
}
"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())),
"O" => {
keep = (!vm.is_none(value)).then(|| value.to_owned());
SimpleStorageValue::ObjectId(value.get_id())
}
"z" | "Z" | "P" | "X" => {
// X preserves opaque-address/null writes. BSTR string assignment
// needs COM allocation and ownership, not a Z-style wide buffer.
let address = if vm.is_none(value) {
0
} else if type_code == "z"
&& let Some(bytes) = value.downcast_ref::<PyBytes>()
{
let (holder, address) = super::base::ensure_z_null_terminated(bytes, vm);
keep = Some(value.to_owned());
hidden = Some(holder);
address
} else if type_code == "Z"
&& let Some(text) = value.downcast_ref::<PyStr>()
{
let (holder, address) = super::base::str_to_wchar_bytes(text.as_wtf8(), vm);
keep = Some(holder);
address
} else if let Some(integer) = value.downcast_ref::<PyInt>() {
bigint_to_i128_wrapping(integer.as_bigint()) as usize
} else {
return Err(vm.new_type_error("integer address expected"));
};
SimpleStorageValue::Pointer(address)
}
_ => {
let index;
let integer = if let Some(integer) = value.downcast_ref_if_exact::<PyInt>(vm) {
integer
} else {
index = value.try_index(vm)?;
&index
};
SimpleStorageValue::Signed(bigint_to_i128_wrapping(integer.as_bigint()))
}
};
Ok(simple_storage_value_to_bytes_endian(type_code, value, true))
let bytes = match (type_code, storage) {
(
"b" | "B" | "h" | "H" | "i" | "I" | "l" | "L" | "q" | "Q",
SimpleStorageValue::Signed(value),
) => {
let size = simple_type_size(type_code).expect("integer ctypes size");
let mut bytes = (value as u64).to_le_bytes();
if super::base::is_big_endian(swapped) {
bytes[..size].reverse();
}
CDataBytes::Integer { bytes, size }
}
(_, storage) => simple_storage_value_to_bytes_endian(type_code, storage, swapped).into(),
};
Ok((bytes, keep, hidden))
}
fn value_to_bytes_endian(
@@ -987,7 +1082,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.keep_alive(0, kept_alive);
cdata.keep_alive(kept_alive);
return Self(cdata).into_ref_with_type(vm, cls).map(Into::into);
}
} else if _type_ == "Z"
@@ -1200,9 +1295,9 @@ impl Py<PyCSimple> {
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" {
// Raw z/Z addresses and None do not replace the old owner: the
// address may still point into its buffer (CPython KeepRef(None)).
if type_code == "O" {
zelf.0.keep_value(value, None, vm)?;
}
Ok(())
+123
View File
@@ -855,3 +855,126 @@ except TypeError:
else:
raise AssertionError("array accepted for an incomplete pointer field")
assert bytes(_field) == _before
# Indexed writes use the element's conversion, not the input object's layout.
for _container, _input, _expected in (
(ctypes.pointer(ctypes.c_float()), 1.5, 1.5),
((ctypes.c_char * 1)(), ctypes.c_char(b"x"), b"x"),
((ctypes.c_byte * 1)(), 257, 1),
):
_container[0] = _input
assert _container[0] == _expected
for _container in ((ctypes.c_int * 1)(), ctypes.pointer(ctypes.c_int())):
try:
_container[0] = ctypes.c_float(1.5)
except TypeError:
pass
else:
raise AssertionError("incompatible indexed value accepted")
assert _container[0] == 0
class FailingFloat:
def __float__(self):
raise LookupError("conversion failed")
for _base in (ctypes.Structure, ctypes.Union):
class ObjectAndFloat(_base):
_fields_ = [("object", ctypes.py_object), ("number", ctypes.c_float)]
_record = ObjectAndFloat()
_value = object()
_record.object = _value
assert _record.object is _value
_before = bytes(_record)
try:
_record.number = FailingFloat()
except LookupError:
pass
else:
raise AssertionError("conversion error suppressed")
assert bytes(_record) == _before
# Tuple temporaries are finalized after their bytes have reached the target.
_events = []
class TemporaryRecord(ctypes.Structure):
_fields_ = [("number", ctypes.c_int)]
def __del__(self):
if self._b_base_ is None:
_events.append(_records[0].number)
_records[0].number = 99
_records = (TemporaryRecord * 1)()
_records[0] = (42,)
assert _events == [42]
assert _records[0].number == 99
# Raw addresses can point inside the buffer already owned by a scalar view.
_events = []
class OwnedBytes(bytes):
def __del__(self):
_events.append("released")
class StringPointer(ctypes.c_char_p):
pass
class OwnedStringField(ctypes.Structure):
_fields_ = [("text", StringPointer)]
_record = OwnedStringField()
_record.text.value = OwnedBytes(b"owner")
_address = ctypes.cast(_record.text, ctypes.c_void_p).value
_record.text.value = _address + 1
gc.collect()
assert _events == []
assert _record.text.value == b"wner"
# Self-copy must not build a new private holder around every previous copy.
_marker = b"ctypes-self-copy-backing-store"
_strings = (StringField * 1)(StringField(_marker))
for _ in range(32):
_strings[0] = _strings[0]
gc.collect()
assert (
sum(
type(obj) is tuple
and any(type(item) is bytes and item == _marker + b"\0" for item in obj)
for obj in gc.get_objects()
)
<= 1
)
assert _strings[0].text == _marker
# Mutating _type_ must not change the initialized setter or its write width.
for _code in ("h", "q", "f"):
class MutableInt(ctypes.c_int):
pass
_array = (MutableInt * 2)(0, 0x1234)
MutableInt._type_ = _code
_array[0] = -2
assert bytes(_array) == bytes(ctypes.c_int(-2)) + bytes(ctypes.c_int(0x1234))
if _sys.platform == "win32":
class BSTR(ctypes._SimpleCData):
_type_ = "X"
_bstr = (BSTR * 1)()
_bstr[0] = None
assert bytes(_bstr) == bytes(ctypes.sizeof(ctypes.c_void_p))