impl more ctypes

- Add __pointer_type__ getter/setter and StgInfo cache
- Add kept_refs (HashMap-keyed) for c_char_p lifetime
- Simplify ensure_z_null_terminated to 2-value return
- Add RawMemoryBuffer for zero-copy memoryview_at_addr
- Add bitfield support in PyCField with validation
- Add CArgObject offset handling in extract_ptr_from_arg
- Add deprecation warning for _pack_ without _layout_
- Fix comparison TypeError message format
- Fix unsupported binop error message format
- Mark failing CI tests as expectedFailure/skip
This commit is contained in:
Jeong, YunWon committed 2026-02-18 03:10:40 +09:00
1 parent 36a53ddf66
commit 38d54fa0cd
15 files changed
+859 -104

No files matched your search

+2
View File
@@ -55,6 +55,8 @@ class TestNullDlsym(unittest.TestCase):
this 'dlsym returned NULL -> throw Error' rule.
"""
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_null_dlsym(self):
import subprocess
import tempfile
+4
View File
@@ -26,6 +26,8 @@ else:
)
class ListSharedLibraries(unittest.TestCase):
# TODO: RUSTPYTHON
@unittest.skipIf(not APPLE, "TODO: RUSTPYTHON")
def test_lists_system(self):
dlls = ctypes.util.dllist()
@@ -34,6 +36,8 @@ class ListSharedLibraries(unittest.TestCase):
any(lib in dll for dll in dlls for lib in KNOWN_LIBRARIES), f"loaded={dlls}"
)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_lists_updates(self):
dlls = ctypes.util.dllist()
+4
View File
@@ -7,6 +7,8 @@ from ctypes import (pythonapi, POINTER, create_string_buffer, sizeof,
class PythonAPITestCase(unittest.TestCase):
# TODO: RUSTPYTHON - requires pythonapi (Python C API)
@unittest.expectedFailure
def test_PyBytes_FromStringAndSize(self):
PyBytes_FromStringAndSize = pythonapi.PyBytes_FromStringAndSize
@@ -57,6 +59,8 @@ class PythonAPITestCase(unittest.TestCase):
del pyobj
self.assertEqual(sys.getrefcount(s), ref)
# TODO: RUSTPYTHON - requires pythonapi (Python C API)
@unittest.expectedFailure
def test_PyOS_snprintf(self):
PyOS_snprintf = pythonapi.PyOS_snprintf
PyOS_snprintf.argtypes = POINTER(c_char), c_size_t, c_char_p
+4
View File
@@ -39,6 +39,8 @@ class ValuesTestCase(unittest.TestCase):
class PythonValuesTestCase(unittest.TestCase):
"""This test only works when python itself is a dll/shared library"""
# TODO: RUSTPYTHON - requires pythonapi (Python C API)
@unittest.expectedFailure
def test_optimizeflag(self):
# This test accesses the Py_OptimizeFlag integer, which is
# exported by the Python dll and should match the sys.flags value
@@ -46,6 +48,8 @@ class PythonValuesTestCase(unittest.TestCase):
opt = c_int.in_dll(pythonapi, "Py_OptimizeFlag").value
self.assertEqual(opt, sys.flags.optimize)
# TODO: RUSTPYTHON - requires pythonapi (Python C API)
@unittest.expectedFailure
@thread_unsafe('overrides frozen modules')
def test_frozentable(self):
# Python exports a PyImport_FrozenModules symbol. This is a
+2
View File
@@ -13,6 +13,8 @@ from ._support import Py_TPFLAGS_DISALLOW_INSTANTIATION, Py_TPFLAGS_IMMUTABLETYP
@unittest.skipUnless(sys.platform == "win32", 'Windows-specific test')
class FunctionCallTestCase(unittest.TestCase):
# TODO: RUSTPYTHON: SEH not implemented, crashes with STATUS_ACCESS_VIOLATION
@unittest.skip("TODO: RUSTPYTHON")
@unittest.skipUnless('MSC' in sys.version, "SEH only supported by MSC")
@unittest.skipIf(sys.executable.lower().endswith('_d.exe'),
"SEH not enabled in debug builds")
+6 -1
View File
@@ -322,7 +322,12 @@ impl PyObject {
match op {
PyComparisonOp::Eq => Ok(Either::B(self.is(&other))),
PyComparisonOp::Ne => Ok(Either::B(!self.is(&other))),
_ => Err(vm.new_unsupported_bin_op_error(self, other, op.operator_token())),
_ => Err(vm.new_type_error(format!(
"'{}' not supported between instances of '{}' and '{}'",
op.operator_token(),
self.class().name(),
other.class().name()
))),
}
}
#[inline(always)]
+16 -2
View File
@@ -70,18 +70,26 @@ impl Py<PyType> {
}
impl PyType {
/// Check if StgInfo is already initialized - prevent double initialization
/// Check if StgInfo is already initialized.
/// Raises SystemError if already initialized.
pub(crate) fn check_not_initialized(&self, vm: &VirtualMachine) -> PyResult<()> {
if let Some(stg_info) = self.get_type_data::<StgInfo>()
&& stg_info.initialized
{
return Err(vm.new_exception_msg(
vm.ctx.exceptions.system_error.to_owned(),
format!("StgInfo of '{}' is already initialized.", self.name()),
format!("class \"{}\" already initialized", self.name()),
));
}
Ok(())
}
/// Check if StgInfo is already initialized, returning true if so.
/// Unlike check_not_initialized, does not raise an error.
pub(crate) fn is_initialized(&self) -> bool {
self.get_type_data::<StgInfo>()
.is_some_and(|stg_info| stg_info.initialized)
}
}
// Dynamic type check helpers for PyCData
@@ -988,6 +996,11 @@ pub(crate) mod _ctypes {
super::function::INTERNAL_CAST_ADDR
}
#[pyattr]
fn _memoryview_at_addr(_vm: &VirtualMachine) -> usize {
super::function::INTERNAL_MEMORYVIEW_AT_ADDR
}
#[pyfunction]
fn _cast(
obj: PyObjectRef,
@@ -1293,6 +1306,7 @@ pub(crate) mod _ctypes {
structure::PyCStructType::make_class(ctx);
union::PyCUnionType::make_class(ctx);
function::PyCFuncPtrType::make_class(ctx);
function::RawMemoryBuffer::make_class(ctx);
extend_module!(vm, module, {
"_CData" => PyCData::make_class(ctx),
+13 -2
View File
@@ -301,6 +301,16 @@ impl Initializer for PyCArrayType {
#[pyclass(flags(IMMUTABLETYPE), with(Initializer, AsNumber))]
impl PyCArrayType {
#[pygetset(name = "__pointer_type__")]
fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult {
super::base::pointer_type_get(&zelf, vm)
}
#[pygetset(name = "__pointer_type__", setter)]
fn set_pointer_type(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
super::base::pointer_type_set(&zelf, value, vm)
}
#[pymethod]
fn from_param(zelf: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// zelf is the array type class that from_param was called on
@@ -777,8 +787,9 @@ impl PyCArray {
let (ptr_val, converted) = if value.is(&vm.ctx.none) {
(0usize, None)
} else if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let (c, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
(ptr, Some(c))
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
zelf.0.keep_alive(index, kept_alive);
(ptr, Some(value.to_owned()))
} else if let Ok(int_val) = value.try_index(vm) {
(int_val.as_bigint().to_usize().unwrap_or(0), None)
} else {
+330 -66
View File
@@ -1,9 +1,9 @@
use super::array::{WCHAR_SIZE, wchar_from_bytes, wchar_to_bytes};
use crate::builtins::{PyBytes, PyDict, PyMemoryView, PyStr, PyType, PyTypeRef};
use crate::builtins::{PyBytes, PyDict, PyMemoryView, PyStr, PyTuple, PyType, PyTypeRef};
use crate::class::StaticType;
use crate::function::{ArgBytesLike, OptionalArg, PySetterValue};
use crate::protocol::{BufferMethods, PyBuffer};
use crate::types::{GetDescriptor, Representable};
use crate::types::{Constructor, GetDescriptor, Representable};
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
};
@@ -97,6 +97,9 @@ pub struct StgInfo {
// FFI field types for structure/union passing (inherited from base class)
pub ffi_field_types: Vec<libffi::middle::Type>,
// Cached pointer type (non-inheritable via descriptor)
pub pointer_type: Option<PyObjectRef>,
}
// StgInfo is stored in type_data which requires Send + Sync.
@@ -141,6 +144,7 @@ impl Default for StgInfo {
paramfunc: ParamFunc::None,
big_endian: cfg!(target_endian = "big"), // native endian by default
ffi_field_types: Vec::new(),
pointer_type: None,
}
}
}
@@ -161,6 +165,7 @@ impl StgInfo {
paramfunc: ParamFunc::None,
big_endian: cfg!(target_endian = "big"), // native endian by default
ffi_field_types: Vec::new(),
pointer_type: None,
}
}
@@ -212,6 +217,7 @@ impl StgInfo {
paramfunc: ParamFunc::Array,
big_endian: cfg!(target_endian = "big"), // native endian by default
ffi_field_types: Vec::new(),
pointer_type: None,
}
}
@@ -293,6 +299,34 @@ impl StgInfo {
}
}
/// __pointer_type__ getter for ctypes metaclasses.
/// Reads from StgInfo.pointer_type (non-inheritable).
pub(super) fn pointer_type_get(zelf: &Py<PyType>, vm: &VirtualMachine) -> PyResult {
zelf.stg_info_opt()
.and_then(|info| info.pointer_type.clone())
.ok_or_else(|| {
vm.new_attribute_error(format!(
"type {} has no attribute '__pointer_type__'",
zelf.name()
))
})
}
/// __pointer_type__ setter for ctypes metaclasses.
/// Writes to StgInfo.pointer_type (non-inheritable).
pub(super) fn pointer_type_set(
zelf: &Py<PyType>,
value: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
if let Some(mut info) = zelf.get_type_data_mut::<StgInfo>() {
info.pointer_type = Some(value);
Ok(())
} else {
Err(vm.new_attribute_error(format!("cannot set __pointer_type__ on {}", zelf.name())))
}
}
/// Get PEP3118 format string for a field type
/// Returns the format string considering byte order
pub(super) fn get_field_format(
@@ -380,25 +414,20 @@ fn vec_to_bytes<T>(vec: Vec<T>) -> Vec<u8> {
unsafe { Vec::from_raw_parts(ptr, len, cap) }
}
/// Ensure PyBytes is null-terminated. Returns (PyBytes to keep, pointer).
/// If already contains null, returns original. Otherwise creates new with null appended.
/// Ensure PyBytes data is null-terminated. Returns (kept_alive_obj, pointer).
/// The caller must keep the returned object alive to keep the pointer valid.
pub(super) fn ensure_z_null_terminated(
bytes: &PyBytes,
vm: &VirtualMachine,
) -> (PyObjectRef, usize) {
let data = bytes.as_bytes();
if data.contains(&0) {
// Already has null, use original
let original: PyObjectRef = vm.ctx.new_bytes(data.to_vec()).into();
(original, data.as_ptr() as usize)
} else {
// Create new with null appended
let mut buffer = data.to_vec();
let mut buffer = data.to_vec();
if !buffer.ends_with(&[0]) {
buffer.push(0);
let ptr = buffer.as_ptr() as usize;
let new_bytes: PyObjectRef = vm.ctx.new_bytes(buffer).into();
(new_bytes, ptr)
}
let ptr = buffer.as_ptr() as usize;
let kept_alive: PyObjectRef = vm.ctx.new_bytes(buffer).into();
(kept_alive, ptr)
}
/// Convert str to null-terminated wchar_t buffer. Returns (PyBytes holder, pointer).
@@ -434,6 +463,11 @@ pub struct PyCData {
pub objects: PyRwLock<Option<PyObjectRef>>,
/// number of references we need (b_length)
pub length: AtomicCell<usize>,
/// References kept alive but not visible in _objects.
/// 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>>,
}
impl PyCData {
@@ -446,6 +480,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()),
}
}
@@ -458,6 +493,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(objects),
length: AtomicCell::new(0),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
}
}
@@ -474,6 +510,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(objects),
length: AtomicCell::new(length),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
}
}
@@ -494,6 +531,7 @@ impl PyCData {
index: AtomicCell::new(0),
objects: PyRwLock::new(None),
length: AtomicCell::new(0),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
}
}
@@ -516,6 +554,7 @@ impl PyCData {
index: AtomicCell::new(idx),
objects: PyRwLock::new(None),
length: AtomicCell::new(length),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
}
}
@@ -542,6 +581,7 @@ impl PyCData {
index: AtomicCell::new(idx),
objects: PyRwLock::new(None),
length: AtomicCell::new(0),
kept_refs: PyRwLock::new(std::collections::HashMap::new()),
}
}
@@ -576,6 +616,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()),
}
}
@@ -797,6 +838,22 @@ impl PyCData {
Ok(())
}
/// 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) {
let key = self.unique_key(index);
if let Some(base_obj) = self.base.read().clone() {
let root = Self::find_root_object(&base_obj);
if let Some(cdata) = root.downcast_ref::<PyCData>() {
cdata.kept_refs.write().insert(key, obj);
return;
}
}
self.kept_refs.write().insert(key, obj);
}
/// 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
@@ -942,12 +999,68 @@ impl PyCData {
return Ok(());
}
// 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.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
.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 mut result = vec![0u8; size];
let addr_bytes = ptr.to_ne_bytes();
let len = core::cmp::min(addr_bytes.len(), size);
result[..len].copy_from_slice(&addr_bytes[..len]);
if needs_swap {
result.reverse();
}
self.write_bytes_at_offset(offset, &result);
self.keep_ref(index, value, vm)?;
self.keep_alive(index, kept_alive);
return Ok(());
}
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 {
@@ -1111,7 +1224,7 @@ impl PyCData {
// CDataType_methods - shared across all ctypes types
#[pyclassmethod]
fn from_buffer(
pub(super) fn from_buffer(
cls: PyTypeRef,
source: PyObjectRef,
offset: OptionalArg<isize>,
@@ -1122,7 +1235,7 @@ impl PyCData {
}
#[pyclassmethod]
fn from_buffer_copy(
pub(super) fn from_buffer_copy(
cls: PyTypeRef,
source: ArgBytesLike,
offset: OptionalArg<isize>,
@@ -1134,7 +1247,7 @@ impl PyCData {
}
#[pyclassmethod]
fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult {
pub(super) fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult {
let size = {
let stg_info = cls.stg_info(vm)?;
stg_info.size
@@ -1150,7 +1263,7 @@ impl PyCData {
}
#[pyclassmethod]
fn in_dll(
pub(super) fn in_dll(
cls: PyTypeRef,
dll: PyObjectRef,
name: crate::builtins::PyStrRef,
@@ -1217,78 +1330,84 @@ impl PyCData {
#[pyclass(name = "CField", module = "_ctypes")]
#[derive(Debug, PyPayload)]
pub struct PyCField {
/// Field name
pub(crate) name: String,
/// Byte offset of the field within the structure/union
pub(crate) offset: isize,
/// Encoded size: for bitfields (bit_size << 16) | bit_offset, otherwise byte size
pub(crate) size: isize,
/// Byte size of the underlying type
pub(crate) byte_size_val: isize,
/// Index into PyCData's object array
pub(crate) index: usize,
/// The ctypes type for this field
pub(crate) proto: PyTypeRef,
/// Flag indicating if the field is anonymous (MakeAnonFields sets this)
pub(crate) anonymous: bool,
}
#[inline(always)]
const fn num_bits(size: isize) -> isize {
size >> 16
}
#[inline(always)]
const fn field_size(size: isize) -> isize {
size & 0xFFFF
}
#[inline(always)]
const fn is_bitfield(size: isize) -> bool {
(size >> 16) != 0
/// Bitfield size in bits (0 for non-bitfield)
pub(crate) bitfield_size: u16,
/// Bit offset within the storage unit (only meaningful for bitfields)
pub(crate) bit_offset_val: u16,
}
impl PyCField {
/// Create a new CField descriptor (non-bitfield)
pub fn new(proto: PyTypeRef, offset: isize, size: isize, index: usize) -> Self {
pub fn new(
name: String,
proto: PyTypeRef,
offset: isize,
byte_size: isize,
index: usize,
) -> Self {
Self {
name,
offset,
size,
byte_size_val: byte_size,
index,
proto,
anonymous: false,
bitfield_size: 0,
bit_offset_val: 0,
}
}
/// Create a new CField descriptor for a bitfield
#[allow(dead_code)]
pub fn new_bitfield(
name: String,
proto: PyTypeRef,
offset: isize,
bit_size: u16,
byte_size: isize,
bitfield_size: u16,
bit_offset: u16,
index: usize,
) -> Self {
let encoded_size = ((bit_size as isize) << 16) | (bit_offset as isize);
Self {
name,
offset,
size: encoded_size,
byte_size_val: byte_size,
index,
proto,
anonymous: false,
bitfield_size,
bit_offset_val: bit_offset,
}
}
/// Get the actual byte size (for non-bitfields) or bit storage size (for bitfields)
pub fn byte_size(&self) -> usize {
field_size(self.size) as usize
/// Get the byte size of the field's underlying type
pub fn get_byte_size(&self) -> usize {
self.byte_size_val as usize
}
/// Create a new CField from an existing field with adjusted offset and index
/// Used by MakeFields to promote anonymous fields
pub fn new_from_field(fdescr: &PyCField, index_offset: usize, offset_delta: isize) -> Self {
Self {
name: fdescr.name.clone(),
offset: fdescr.offset + offset_delta,
size: fdescr.size,
byte_size_val: fdescr.byte_size_val,
index: fdescr.index + index_offset,
proto: fdescr.proto.clone(),
anonymous: false, // promoted fields are not anonymous themselves
bitfield_size: fdescr.bitfield_size,
bit_offset_val: fdescr.bit_offset_val,
}
}
@@ -1298,6 +1417,107 @@ impl PyCField {
}
}
impl Constructor for PyCField {
type Args = crate::function::FuncArgs;
fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
// PyCField_new_impl: requires _internal_use=True
let internal_use = if let Some(v) = args.kwargs.get("_internal_use") {
v.clone().try_to_bool(vm)?
} else {
false
};
if !internal_use {
return Err(vm.new_type_error(
"CField is not intended to be used directly; use it via Structure or Union fields"
.to_string(),
));
}
let name: String = args
.kwargs
.get("name")
.ok_or_else(|| vm.new_type_error("missing required argument: 'name'"))?
.try_to_value(vm)?;
let field_type: PyTypeRef = args
.kwargs
.get("type")
.ok_or_else(|| vm.new_type_error("missing required argument: 'type'"))?
.clone()
.downcast()
.map_err(|_| vm.new_type_error("'type' must be a ctypes type"))?;
let byte_size: isize = args
.kwargs
.get("byte_size")
.ok_or_else(|| vm.new_type_error("missing required argument: 'byte_size'"))?
.try_to_value(vm)?;
let byte_offset: isize = args
.kwargs
.get("byte_offset")
.ok_or_else(|| vm.new_type_error("missing required argument: 'byte_offset'"))?
.try_to_value(vm)?;
let index: usize = args
.kwargs
.get("index")
.ok_or_else(|| vm.new_type_error("missing required argument: 'index'"))?
.try_to_value(vm)?;
// Validate byte_size matches the type
let type_size = super::base::get_field_size(field_type.as_object(), vm)? as isize;
if byte_size != type_size {
return Err(vm.new_value_error(format!(
"byte_size {} does not match type size {}",
byte_size, type_size
)));
}
let bit_size_val: Option<isize> = args
.kwargs
.get("bit_size")
.map(|v| v.try_to_value(vm))
.transpose()?;
let bit_offset_val: Option<isize> = args
.kwargs
.get("bit_offset")
.map(|v| v.try_to_value(vm))
.transpose()?;
if let Some(bs) = bit_size_val {
if bs < 0 {
return Err(vm.new_value_error("number of bits invalid for bit field".to_string()));
}
let bo = bit_offset_val.unwrap_or(0);
if bo < 0 {
return Err(vm.new_value_error("bit_offset must be >= 0".to_string()));
}
let type_bits = byte_size * 8;
if bo + bs > type_bits {
return Err(vm.new_value_error(format!(
"bit field '{}' overflows its type ({} + {} > {})",
name, bo, bs, type_bits
)));
}
Ok(Self::new_bitfield(
name,
field_type,
byte_offset,
byte_size,
bs as u16,
bo as u16,
index,
))
} else {
Ok(Self::new(name, field_type, byte_offset, byte_size, index))
}
}
}
impl Representable for PyCField {
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
// Get type name from proto (which is always PyTypeRef)
@@ -1305,17 +1525,15 @@ impl Representable for PyCField {
// Bitfield: <Field type=TYPE, ofs=OFFSET:BIT_OFFSET, bits=NUM_BITS>
// Regular: <Field type=TYPE, ofs=OFFSET, size=SIZE>
if is_bitfield(zelf.size) {
let bit_offset = field_size(zelf.size);
let bits = num_bits(zelf.size);
if zelf.bitfield_size > 0 {
Ok(format!(
"<Field type={}, ofs={}:{}, bits={}>",
tp_name, zelf.offset, bit_offset, bits
tp_name, zelf.offset, zelf.bit_offset_val, zelf.bitfield_size
))
} else {
Ok(format!(
"<Field type={}, ofs={}, size={}>",
tp_name, zelf.offset, zelf.size
tp_name, zelf.offset, zelf.byte_size_val
))
}
}
@@ -1340,7 +1558,7 @@ impl GetDescriptor for PyCField {
};
let offset = zelf.offset as usize;
let size = zelf.byte_size();
let size = zelf.get_byte_size();
// Get PyCData from obj (works for both Structure and Union)
let cdata = PyCField::get_cdata_from_obj(&obj, vm)?;
@@ -1468,15 +1686,8 @@ impl PyCField {
Ok((f.to_ne_bytes().to_vec(), None))
}
"z" => {
// c_char_p: store pointer to null-terminated bytes
if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let (converted, ptr) = ensure_z_null_terminated(bytes, vm);
let mut result = vec![0u8; size];
let addr_bytes = ptr.to_ne_bytes();
let len = core::cmp::min(addr_bytes.len(), size);
result[..len].copy_from_slice(&addr_bytes[..len]);
return Ok((result, Some(converted)));
}
// 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);
@@ -1583,10 +1794,7 @@ impl PyCField {
}
}
#[pyclass(
flags(DISALLOW_INSTANTIATION, IMMUTABLETYPE),
with(Representable, GetDescriptor)
)]
#[pyclass(flags(IMMUTABLETYPE), with(Representable, GetDescriptor, Constructor))]
impl PyCField {
/// Get PyCData from object (works for both Structure and Union)
fn get_cdata_from_obj<'a>(obj: &'a PyObjectRef, vm: &VirtualMachine) -> PyResult<&'a PyCData> {
@@ -1615,7 +1823,7 @@ impl PyCField {
.ok_or_else(|| vm.new_type_error("expected CField"))?;
let offset = zelf.offset as usize;
let size = zelf.byte_size();
let size = zelf.get_byte_size();
// Get PyCData from obj (works for both Structure and Union)
let cdata = Self::get_cdata_from_obj(&obj, vm)?;
@@ -1650,14 +1858,65 @@ impl PyCField {
}
}
#[pygetset]
fn name(&self) -> String {
self.name.clone()
}
#[pygetset(name = "type")]
fn type_(&self) -> PyTypeRef {
self.proto.clone()
}
#[pygetset]
fn offset(&self) -> isize {
self.offset
}
#[pygetset]
fn byte_offset(&self) -> isize {
self.offset
}
#[pygetset]
fn size(&self) -> isize {
self.size
// Legacy: encode as (bitfield_size << 16) | bit_offset for bitfields
if self.bitfield_size > 0 {
((self.bitfield_size as isize) << 16) | (self.bit_offset_val as isize)
} else {
self.byte_size_val
}
}
#[pygetset]
fn byte_size(&self) -> isize {
self.byte_size_val
}
#[pygetset]
fn bit_offset(&self) -> isize {
self.bit_offset_val as isize
}
#[pygetset]
fn bit_size(&self, vm: &VirtualMachine) -> PyObjectRef {
if self.bitfield_size > 0 {
vm.ctx.new_int(self.bitfield_size).into()
} else {
// Non-bitfield: bit_size = byte_size * 8
let byte_size = self.byte_size_val as i128;
vm.ctx.new_int(byte_size * 8).into()
}
}
#[pygetset]
fn is_bitfield(&self) -> bool {
self.bitfield_size > 0
}
#[pygetset]
fn is_anonymous(&self) -> bool {
self.anonymous
}
}
@@ -2139,7 +2398,12 @@ pub(super) fn set_or_init_stginfo(type_ref: &PyType, stg_info: StgInfo) {
if type_ref.init_type_data(stg_info.clone()).is_err()
&& let Some(mut existing) = type_ref.get_type_data_mut::<StgInfo>()
{
// Preserve pointer_type cache across StgInfo replacement
let old_pointer_type = existing.pointer_type.take();
*existing = stg_info;
if existing.pointer_type.is_none() {
existing.pointer_type = old_pointer_type;
}
}
}
+104 -2
View File
@@ -31,6 +31,7 @@ use rustpython_common::lock::PyRwLock;
pub(super) const INTERNAL_CAST_ADDR: usize = 1;
pub(super) const INTERNAL_STRING_AT_ADDR: usize = 2;
pub(super) const INTERNAL_WSTRING_AT_ADDR: usize = 3;
pub(super) const INTERNAL_MEMORYVIEW_AT_ADDR: usize = 4;
// Thread-local errno storage for ctypes
std::thread_local! {
@@ -512,7 +513,17 @@ impl Initializer for PyCFuncPtrType {
}
#[pyclass(flags(IMMUTABLETYPE), with(Initializer))]
impl PyCFuncPtrType {}
impl PyCFuncPtrType {
#[pygetset(name = "__pointer_type__")]
fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult {
super::base::pointer_type_get(&zelf, vm)
}
#[pygetset(name = "__pointer_type__", setter)]
fn set_pointer_type(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
super::base::pointer_type_set(&zelf, value, vm)
}
}
/// PyCFuncPtr - Function pointer instance
/// Saved in _base.buffer
@@ -561,6 +572,16 @@ impl Debug for PyCFuncPtr {
/// Extract pointer value from a ctypes argument (c_void_p conversion)
fn extract_ptr_from_arg(arg: &PyObject, vm: &VirtualMachine) -> PyResult<usize> {
// Try CArgObject first - extract the wrapped pointer value, applying offset
if let Some(carg) = arg.downcast_ref::<super::_ctypes::CArgObject>() {
if carg.offset != 0
&& let Some(cdata) = carg.obj.downcast_ref::<PyCData>()
{
let base = cdata.buffer.read().as_ptr() as isize;
return Ok((base + carg.offset) as usize);
}
return extract_ptr_from_arg(&carg.obj, vm);
}
// Try to get pointer value from various ctypes types
if let Some(ptr) = arg.downcast_ref::<PyCPointer>() {
return Ok(ptr.get_ptr_value());
@@ -655,6 +676,68 @@ fn wstring_at_impl(ptr: usize, size: isize, vm: &VirtualMachine) -> PyResult {
}
}
/// A buffer wrapping raw memory at a given pointer, for zero-copy memoryview.
#[pyclass(name = "_RawMemoryBuffer", module = "_ctypes")]
#[derive(Debug, PyPayload)]
pub(super) struct RawMemoryBuffer {
ptr: *const u8,
size: usize,
readonly: bool,
}
// SAFETY: The caller ensures the pointer remains valid
unsafe impl Send for RawMemoryBuffer {}
unsafe impl Sync for RawMemoryBuffer {}
static RAW_MEMORY_BUFFER_METHODS: crate::protocol::BufferMethods = crate::protocol::BufferMethods {
obj_bytes: |buffer| {
let raw = buffer.obj_as::<RawMemoryBuffer>();
let slice = unsafe { core::slice::from_raw_parts(raw.ptr, raw.size) };
rustpython_common::borrow::BorrowedValue::Ref(slice)
},
obj_bytes_mut: |buffer| {
let raw = buffer.obj_as::<RawMemoryBuffer>();
let slice = unsafe { core::slice::from_raw_parts_mut(raw.ptr as *mut u8, raw.size) };
rustpython_common::borrow::BorrowedValueMut::RefMut(slice)
},
release: |_| {},
retain: |_| {},
};
#[pyclass(with(AsBuffer))]
impl RawMemoryBuffer {}
impl AsBuffer for RawMemoryBuffer {
fn as_buffer(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyBuffer> {
Ok(PyBuffer::new(
zelf.to_owned().into(),
BufferDescriptor::simple(zelf.size, zelf.readonly),
&RAW_MEMORY_BUFFER_METHODS,
))
}
}
/// memoryview_at implementation - create a memoryview from memory at ptr
fn memoryview_at_impl(ptr: usize, size: isize, readonly: bool, vm: &VirtualMachine) -> PyResult {
use crate::builtins::PyMemoryView;
if ptr == 0 {
return Err(vm.new_value_error("NULL pointer access"));
}
if size < 0 {
return Err(vm.new_value_error("negative size"));
}
let len = size as usize;
let raw_buf = RawMemoryBuffer {
ptr: ptr as *const u8,
size: len,
readonly,
}
.into_pyobject(vm);
let mv = PyMemoryView::from_object(&raw_buf, vm)?;
Ok(mv.into_pyobject(vm))
}
// cast_check_pointertype
fn cast_check_pointertype(ctype: &PyObject, vm: &VirtualMachine) -> bool {
use super::pointer::PyCPointerType;
@@ -1064,6 +1147,25 @@ fn handle_internal_func(addr: usize, args: &FuncArgs, vm: &VirtualMachine) -> Op
}));
}
if addr == INTERNAL_MEMORYVIEW_AT_ADDR {
let result: PyResult<(PyObjectRef, PyObjectRef, Option<PyObjectRef>)> =
args.clone().bind(vm);
return Some(result.and_then(|(ptr_arg, size_arg, readonly_arg)| {
let ptr = extract_ptr_from_arg(&ptr_arg, vm)?;
let size_int = size_arg.try_int(vm)?;
let size = size_int
.as_bigint()
.to_isize()
.ok_or_else(|| vm.new_value_error("size too large"))?;
let readonly = readonly_arg
.and_then(|r| r.try_int(vm).ok())
.and_then(|i| i.as_bigint().to_i32())
.unwrap_or(0)
!= 0;
memoryview_at_impl(ptr, size, readonly, vm)
}));
}
None
}
@@ -2091,7 +2193,7 @@ unsafe extern "C" fn thunk_callback(
.map(|s| s.to_string())
.unwrap_or_else(|_| "<unknown>".to_string());
let msg = format!(
"Exception ignored on calling ctypes callback function {}",
"Exception ignored while calling ctypes callback function {}",
repr
);
vm.run_unraisable(exc.clone(), Some(msg), vm.ctx.none());
+39 -6
View File
@@ -27,16 +27,24 @@ impl Initializer for PyCPointerType {
.downcast()
.map_err(|_| vm.new_type_error("expected type"))?;
new_type.check_not_initialized(vm)?;
if new_type.is_initialized() {
return Ok(());
}
// Get the _type_ attribute (element type)
// PyCPointerType_init gets the element type from _type_ attribute
let proto = new_type
.as_object()
.get_attr("_type_", vm)
.ok()
.and_then(|obj| obj.downcast::<PyType>().ok());
// Validate that _type_ has storage info (is a ctypes type)
if let Some(ref proto_type) = proto
&& proto_type.stg_info_opt().is_none()
{
return Err(vm.new_type_error(format!("{} must have storage info", proto_type.name())));
}
// Initialize StgInfo for pointer type
let pointer_size = core::mem::size_of::<usize>();
let mut stg_info = StgInfo::new(pointer_size, pointer_size);
@@ -62,12 +70,31 @@ impl Initializer for PyCPointerType {
let _ = new_type.init_type_data(stg_info);
// Cache: set target_type.__pointer_type__ = self (via StgInfo, not as inheritable attr)
if let Ok(type_attr) = new_type.as_object().get_attr("_type_", vm)
&& let Ok(target_type) = type_attr.downcast::<PyType>()
&& let Some(mut target_info) = target_type.get_type_data_mut::<StgInfo>()
{
let zelf_obj: PyObjectRef = zelf.into();
target_info.pointer_type = Some(zelf_obj);
}
Ok(())
}
}
#[pyclass(flags(IMMUTABLETYPE), with(AsNumber, Initializer))]
#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(AsNumber, Initializer))]
impl PyCPointerType {
#[pygetset(name = "__pointer_type__")]
fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult {
super::base::pointer_type_get(&zelf, vm)
}
#[pygetset(name = "__pointer_type__", setter)]
fn set_pointer_type(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
super::base::pointer_type_set(&zelf, value, vm)
}
#[pymethod]
fn from_param(zelf: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// zelf is the pointer type class that from_param was called on
@@ -182,7 +209,12 @@ impl PyCPointerType {
}
// 4. Set _type_ attribute on the pointer type
zelf.as_object().set_attr("_type_", typ_type, vm)?;
zelf.as_object().set_attr("_type_", typ_type.clone(), vm)?;
// 5. Cache: set target_type.__pointer_type__ = self (via StgInfo)
if let Some(mut target_info) = typ_type.get_type_data_mut::<StgInfo>() {
target_info.pointer_type = Some(zelf.into());
}
Ok(())
}
@@ -598,11 +630,12 @@ impl PyCPointer {
if type_code.as_deref() == Some("z")
&& let Some(bytes) = value.downcast_ref::<PyBytes>()
{
let (converted, ptr_val) = super::base::ensure_z_null_terminated(bytes, vm);
let (kept_alive, ptr_val) = super::base::ensure_z_null_terminated(bytes, vm);
unsafe {
*(addr as *mut usize) = ptr_val;
}
return zelf.0.keep_ref(index as usize, converted, vm);
zelf.0.keep_alive(index as usize, kept_alive);
return zelf.0.keep_ref(index as usize, value.clone(), vm);
} else if type_code.as_deref() == Some("Z")
&& let Some(s) = value.downcast_ref::<PyStr>()
{
+24 -8
View File
@@ -18,8 +18,12 @@ use core::fmt::Debug;
use num_traits::ToPrimitive;
/// Valid type codes for ctypes simple types
#[cfg(windows)]
// spell-checker: disable-next-line
pub(super) const SIMPLE_TYPE_CHARS: &str = "cbBhHiIlLdfuzZqQPXOv?g";
#[cfg(not(windows))]
// spell-checker: disable-next-line
pub(super) const SIMPLE_TYPE_CHARS: &str = "cbBhHiIlLdfuzZqQPOv?g";
/// Convert ctypes type code to PEP 3118 format code.
/// Some ctypes codes need to be mapped to standard-size codes based on platform.
@@ -235,6 +239,16 @@ pub struct PyCSimpleType(PyType);
#[pyclass(flags(BASETYPE), with(AsNumber, Initializer))]
impl PyCSimpleType {
#[pygetset(name = "__pointer_type__")]
fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult {
super::base::pointer_type_get(&zelf, vm)
}
#[pygetset(name = "__pointer_type__", setter)]
fn set_pointer_type(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
super::base::pointer_type_set(&zelf, value, vm)
}
#[allow(clippy::new_ret_no_self)]
#[pymethod]
fn new(cls: PyTypeRef, _: OptionalArg, vm: &VirtualMachine) -> PyResult {
@@ -327,10 +341,10 @@ impl PyCSimpleType {
Some("z") => {
// 1. bytes → create CArgObject with null-terminated buffer
if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let (holder, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
return Ok(CArgObject {
tag: b'z',
value: FfiArgValue::OwnedPointer(ptr, holder),
value: FfiArgValue::OwnedPointer(ptr, kept_alive),
obj: value.clone(),
size: 0,
offset: 0,
@@ -381,10 +395,10 @@ impl PyCSimpleType {
}
// 2. bytes → create CArgObject with null-terminated buffer
if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let (holder, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
return Ok(CArgObject {
tag: b'z',
value: FfiArgValue::OwnedPointer(ptr, holder),
value: FfiArgValue::OwnedPointer(ptr, kept_alive),
obj: value.clone(),
size: 0,
offset: 0,
@@ -1027,9 +1041,10 @@ impl Constructor for PyCSimple {
if let Some(ref v) = init_arg {
if _type_ == "z" {
if let Some(bytes) = v.downcast_ref::<PyBytes>() {
let (converted, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
let buffer = ptr.to_ne_bytes().to_vec();
let cdata = PyCData::from_bytes(buffer, Some(converted));
let cdata = PyCData::from_bytes(buffer, Some(v.clone()));
*cdata.base.write() = Some(kept_alive);
return PyCSimple(cdata).into_ref_with_type(vm, cls).map(Into::into);
}
} else if _type_ == "Z"
@@ -1258,9 +1273,10 @@ impl PyCSimple {
// Handle z/Z types with PyBytes/PyStr separately to avoid memory leak
if type_code == "z" {
if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let (converted, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
let (kept_alive, ptr) = super::base::ensure_z_null_terminated(bytes, vm);
*zelf.0.buffer.write() = alloc::borrow::Cow::Owned(ptr.to_ne_bytes().to_vec());
*zelf.0.objects.write() = Some(converted);
*zelf.0.objects.write() = Some(value);
*zelf.0.base.write() = Some(kept_alive);
return Ok(());
}
} else if type_code == "Z"
+178 -6
View File
@@ -1,9 +1,9 @@
use super::base::{CDATA_BUFFER_METHODS, PyCData, PyCField, StgInfo, StgInfoFlags};
use crate::builtins::{PyList, PyStr, PyTuple, PyType, PyTypeRef};
use crate::convert::ToPyObject;
use crate::function::FuncArgs;
use crate::function::PySetterValue;
use crate::function::{FuncArgs, OptionalArg, PySetterValue};
use crate::protocol::{BufferDescriptor, PyBuffer, PyNumberMethods};
use crate::stdlib::warnings;
use crate::types::{AsBuffer, AsNumber, Constructor, Initializer, SetAttr};
use crate::{AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine};
use alloc::borrow::Cow;
@@ -103,6 +103,7 @@ impl Initializer for PyCStructType {
let mut stg_info = baseinfo.clone();
stg_info.flags &= !StgInfoFlags::DICTFLAG_FINAL; // Clear FINAL in subclass
stg_info.initialized = true;
stg_info.pointer_type = None; // Non-inheritable
stg_info
});
@@ -130,6 +131,16 @@ impl Initializer for PyCStructType {
#[pyclass(flags(BASETYPE), with(AsNumber, Constructor, Initializer, SetAttr))]
impl PyCStructType {
#[pygetset(name = "__pointer_type__")]
fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult {
super::base::pointer_type_get(&zelf, vm)
}
#[pygetset(name = "__pointer_type__", setter)]
fn set_pointer_type(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
super::base::pointer_type_set(&zelf, value, vm)
}
#[pymethod]
fn from_param(zelf: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// zelf is the structure type class that from_param was called on
@@ -154,6 +165,55 @@ impl PyCStructType {
)))
}
// CDataType methods - delegated to PyCData implementations
#[pymethod]
fn from_address(zelf: PyObjectRef, address: isize, vm: &VirtualMachine) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::from_address(cls, address, vm)
}
#[pymethod]
fn from_buffer(
zelf: PyObjectRef,
source: PyObjectRef,
offset: OptionalArg<isize>,
vm: &VirtualMachine,
) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::from_buffer(cls, source, offset, vm)
}
#[pymethod]
fn from_buffer_copy(
zelf: PyObjectRef,
source: crate::function::ArgBytesLike,
offset: OptionalArg<isize>,
vm: &VirtualMachine,
) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::from_buffer_copy(cls, source, offset, vm)
}
#[pymethod]
fn in_dll(
zelf: PyObjectRef,
dll: PyObjectRef,
name: crate::builtins::PyStrRef,
vm: &VirtualMachine,
) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::in_dll(cls, dll, name, vm)
}
/// Called when a new Structure subclass is created
#[pyclassmethod]
fn __init_subclass__(cls: PyTypeRef, vm: &VirtualMachine) -> PyResult<()> {
@@ -185,6 +245,24 @@ impl PyCStructType {
};
let pack = super::base::get_usize_attr(cls.as_object(), "_pack_", 0, vm)?;
// Emit DeprecationWarning on non-Windows when _pack_ is set without _layout_
if pack > 0 && !cfg!(windows) {
let has_layout = cls.as_object().get_attr("_layout_", vm).is_ok();
if !has_layout {
let base_type_name = "Structure";
let msg = format!(
"Due to '_pack_', the '{}' {} will use memory layout compatible with \
MSVC (Windows). If this is intended, set _layout_ to 'ms'. \
The implicit default is deprecated and slated to become an error in \
Python 3.19.",
cls.name(),
base_type_name,
);
warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm)?;
}
}
let forced_alignment =
super::base::get_usize_attr(cls.as_object(), "_align_", 1, vm)?.max(1);
@@ -221,6 +299,11 @@ impl PyCStructType {
let mut format = String::from("T{");
let mut last_end = 0usize; // Track end of last field for padding calculation
// Bitfield layout tracking
let mut bitfield_bit_offset: u16 = 0; // Current bit position within bitfield group
let mut last_field_bit_size: u16 = 0; // For MSVC: bit size of previous storage unit
let use_msvc_bitfields = pack > 0; // MSVC layout when _pack_ is set
for (index, field) in fields.iter().enumerate() {
let field_tuple = field
.downcast_ref::<PyTuple>()
@@ -336,14 +419,93 @@ impl PyCStructType {
.clone()
.downcast::<PyType>()
.map_err(|_| vm.new_type_error("_fields_ type must be a ctypes type"))?;
let c_field = PyCField::new(field_type_ref, offset as isize, size as isize, index);
// Check for bitfield size (optional 3rd element in tuple)
let (c_field, field_advances_offset) = if field_tuple.len() > 2 {
let bit_size_obj = field_tuple.get(2).expect("len checked");
let bit_size = bit_size_obj
.try_int(vm)?
.as_bigint()
.to_u16()
.ok_or_else(|| {
vm.new_value_error("number of bits invalid for bit field".to_string())
})?;
has_bitfield = true;
let type_bits = (size * 8) as u16;
let (advances, bit_offset);
if use_msvc_bitfields {
// MSVC layout: different types start new storage unit
if bitfield_bit_offset + bit_size > type_bits
|| type_bits != last_field_bit_size
{
// Close previous bitfield, start new allocation unit
bitfield_bit_offset = 0;
advances = true;
} else {
advances = false;
}
bit_offset = bitfield_bit_offset;
bitfield_bit_offset += bit_size;
last_field_bit_size = type_bits;
} else {
// GCC System V layout: pack within same type
let fits_in_current = bitfield_bit_offset + bit_size <= type_bits;
advances = if fits_in_current && bitfield_bit_offset > 0 {
false
} else if !fits_in_current {
bitfield_bit_offset = 0;
true
} else {
true
};
bit_offset = bitfield_bit_offset;
bitfield_bit_offset += bit_size;
}
// For packed bitfields that share offset, use the same offset as previous
let field_offset = if !advances {
offset - size // Reuse the previous field's offset
} else {
offset
};
(
PyCField::new_bitfield(
name.clone(),
field_type_ref,
field_offset as isize,
size as isize,
bit_size,
bit_offset,
index,
),
advances,
)
} else {
bitfield_bit_offset = 0; // Reset on non-bitfield
last_field_bit_size = 0;
(
PyCField::new(
name.clone(),
field_type_ref,
offset as isize,
size as isize,
index,
),
true,
)
};
// Set the CField as a class attribute
cls.set_attr(vm.ctx.intern_str(name.clone()), c_field.to_pyobject(vm));
// Update tracking
last_end = offset + size;
offset += size;
// Update tracking - don't advance offset for packed bitfields
if field_advances_offset {
last_end = offset + size;
offset += size;
}
}
// Calculate total_align = max(max_align, forced_alignment)
@@ -366,6 +528,16 @@ impl PyCStructType {
}
format.push('}');
// Check for circular self-reference: if a field of the same type as this
// structure was encountered, it would have marked this type's stginfo as FINAL.
if let Some(stg_info) = cls.get_type_data::<StgInfo>()
&& stg_info.is_final()
{
return Err(
vm.new_attribute_error("Structure or union cannot contain itself".to_string())
);
}
// Store StgInfo with aligned size and total alignment
let mut stg_info = StgInfo::new(aligned_size, total_align);
stg_info.length = fields.len();
+132 -10
View File
@@ -2,12 +2,13 @@ use super::base::{CDATA_BUFFER_METHODS, StgInfoFlags};
use super::{PyCData, PyCField, StgInfo};
use crate::builtins::{PyList, PyStr, PyTuple, PyType, PyTypeRef};
use crate::convert::ToPyObject;
use crate::function::FuncArgs;
use crate::function::PySetterValue;
use crate::function::{ArgBytesLike, FuncArgs, OptionalArg, PySetterValue};
use crate::protocol::{BufferDescriptor, PyBuffer};
use crate::stdlib::warnings;
use crate::types::{AsBuffer, Constructor, Initializer, SetAttr};
use crate::{AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine};
use alloc::borrow::Cow;
use num_traits::ToPrimitive;
/// Calculate Union type size from _fields_ (max field size)
pub(super) fn calculate_union_size(cls: &Py<PyType>, vm: &VirtualMachine) -> PyResult<usize> {
@@ -106,6 +107,7 @@ impl Initializer for PyCUnionType {
let mut stg_info = baseinfo.clone();
stg_info.flags &= !StgInfoFlags::DICTFLAG_FINAL; // Clear FINAL flag in subclass
stg_info.initialized = true;
stg_info.pointer_type = None; // Non-inheritable
stg_info
});
@@ -163,6 +165,22 @@ impl PyCUnionType {
};
let pack = super::base::get_usize_attr(cls.as_object(), "_pack_", 0, vm)?;
// Emit DeprecationWarning on non-Windows when _pack_ is set without _layout_
if pack > 0 && !cfg!(windows) {
let has_layout = cls.as_object().get_attr("_layout_", vm).is_ok();
if !has_layout {
let msg = format!(
"Due to '_pack_', the '{}' Union will use memory layout compatible with \
MSVC (Windows). If this is intended, set _layout_ to 'ms'. \
The implicit default is deprecated and slated to become an error in \
Python 3.19.",
cls.name(),
);
warnings::warn(vm.ctx.exceptions.deprecation_warning, msg, 1, vm)?;
}
}
let forced_alignment =
super::base::get_usize_attr(cls.as_object(), "_align_", 1, vm)?.max(1);
@@ -258,7 +276,42 @@ impl PyCUnionType {
.clone()
.downcast::<PyType>()
.map_err(|_| vm.new_type_error("_fields_ type must be a ctypes type"))?;
let c_field = PyCField::new(field_type_ref, 0, size as isize, index);
// Check for bitfield size (optional 3rd element in tuple)
// For unions, each field starts fresh (CPython: _layout.py)
let c_field = if field_tuple.len() > 2 {
let bit_size_obj = field_tuple.get(2).expect("len checked");
let bit_size = bit_size_obj
.try_int(vm)?
.as_bigint()
.to_u16()
.ok_or_else(|| {
vm.new_value_error("number of bits invalid for bit field".to_string())
})?;
has_bitfield = true;
// Union fields all start at offset 0, so bit_offset = 0
let mut bit_offset: u16 = 0;
let type_bits = (size * 8) as u16;
// Big-endian: bit_offset = type_bits - bit_size
let big_endian = is_swapped != cfg!(target_endian = "big");
if big_endian && type_bits >= bit_size {
bit_offset = type_bits - bit_size;
}
PyCField::new_bitfield(
name.clone(),
field_type_ref,
0, // Union fields always at offset 0
size as isize,
bit_size,
bit_offset,
index,
)
} else {
PyCField::new(name.clone(), field_type_ref, 0, size as isize, index)
};
cls.set_attr(vm.ctx.intern_str(name), c_field.to_pyobject(vm));
}
@@ -271,6 +324,15 @@ impl PyCUnionType {
max_size
};
// Check for circular self-reference
if let Some(stg_info) = cls.get_type_data::<StgInfo>()
&& stg_info.is_final()
{
return Err(
vm.new_attribute_error("Structure or union cannot contain itself".to_string())
);
}
// Store StgInfo with aligned size
let mut stg_info = StgInfo::new(aligned_size, total_align);
stg_info.length = fields.len();
@@ -299,6 +361,16 @@ impl PyCUnionType {
#[pyclass(flags(BASETYPE), with(Constructor, Initializer, SetAttr))]
impl PyCUnionType {
#[pygetset(name = "__pointer_type__")]
fn pointer_type(zelf: PyTypeRef, vm: &VirtualMachine) -> PyResult {
super::base::pointer_type_get(&zelf, vm)
}
#[pygetset(name = "__pointer_type__", setter)]
fn set_pointer_type(zelf: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
super::base::pointer_type_set(&zelf, value, vm)
}
#[pymethod]
fn from_param(zelf: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult {
// zelf is the union type class that from_param was called on
@@ -344,6 +416,55 @@ impl PyCUnionType {
)))
}
// CDataType methods - delegated to PyCData implementations
#[pymethod]
fn from_address(zelf: PyObjectRef, address: isize, vm: &VirtualMachine) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::from_address(cls, address, vm)
}
#[pymethod]
fn from_buffer(
zelf: PyObjectRef,
source: PyObjectRef,
offset: OptionalArg<isize>,
vm: &VirtualMachine,
) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::from_buffer(cls, source, offset, vm)
}
#[pymethod]
fn from_buffer_copy(
zelf: PyObjectRef,
source: ArgBytesLike,
offset: OptionalArg<isize>,
vm: &VirtualMachine,
) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::from_buffer_copy(cls, source, offset, vm)
}
#[pymethod]
fn in_dll(
zelf: PyObjectRef,
dll: PyObjectRef,
name: crate::builtins::PyStrRef,
vm: &VirtualMachine,
) -> PyResult {
let cls: PyTypeRef = zelf
.downcast()
.map_err(|_| vm.new_type_error("expected a type"))?;
PyCData::in_dll(cls, dll, name, vm)
}
/// Called when a new Union subclass is created
#[pyclassmethod]
fn __init_subclass__(cls: PyTypeRef, vm: &VirtualMachine) -> PyResult<()> {
@@ -383,6 +504,14 @@ impl SetAttr for PyCUnionType {
}
}
// 2. If _fields_, call process_fields (which checks FINAL internally)
// Check BEFORE writing to dict to avoid storing _fields_ when FINAL
if attr_name.as_str() == "_fields_"
&& let PySetterValue::Assign(ref fields_value) = value
{
PyCUnionType::process_fields(pytype, fields_value.clone(), vm)?;
}
// Store in type's attributes dict
match &value {
PySetterValue::Assign(v) => {
@@ -403,13 +532,6 @@ impl SetAttr for PyCUnionType {
}
}
// 2. If _fields_, call process_fields (which checks FINAL internally)
if attr_name.as_str() == "_fields_"
&& let PySetterValue::Assign(fields_value) = value
{
PyCUnionType::process_fields(pytype, fields_value, vm)?;
}
Ok(())
}
}
+1 -1
View File
@@ -211,7 +211,7 @@ impl VirtualMachine {
op: &str,
) -> PyBaseExceptionRef {
self.new_type_error(format!(
"'{}' not supported between instances of '{}' and '{}'",
"unsupported operand type(s) for {}: '{}' and '{}'",
op,
a.class().name(),
b.class().name()