diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 5676549ec1..19c8725fa6 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -50,6 +50,7 @@ stackdepth stringlib structseq subparams +swappedbytes ticketer tok_oldval tvars diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 46fc16c5ed..1db02dd17a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -44,7 +44,7 @@ cargo run -- script.py cargo run # With specific features -cargo run --features ssl +cargo run --features jit # Release mode (recommended for better performance) cargo run --release -- script.py @@ -176,13 +176,6 @@ cargo build --target wasm32-wasip1 --no-default-features --features freeze-stdli cargo run --features jit ``` -### SSL Support - -```bash -# Enable SSL support -cargo run --features ssl -``` - ## Test Code Modification Rules **CRITICAL: Test code modification restrictions** diff --git a/Lib/ctypes/wintypes.py b/Lib/ctypes/wintypes.py index c619d27596..9c4e721438 100644 --- a/Lib/ctypes/wintypes.py +++ b/Lib/ctypes/wintypes.py @@ -1,7 +1,7 @@ # The most useful windows datatypes import ctypes -BYTE = ctypes.c_byte +BYTE = ctypes.c_ubyte WORD = ctypes.c_ushort DWORD = ctypes.c_ulong diff --git a/crates/vm/src/stdlib/ctypes.rs b/crates/vm/src/stdlib/ctypes.rs index 7e9557458c..7c35d4a700 100644 --- a/crates/vm/src/stdlib/ctypes.rs +++ b/crates/vm/src/stdlib/ctypes.rs @@ -9,12 +9,14 @@ pub(crate) mod pointer; pub(crate) mod structure; pub(crate) mod thunk; pub(crate) mod union; +pub(crate) mod util; use crate::builtins::PyModule; use crate::class::PyClassImpl; -use crate::stdlib::ctypes::base::{PyCData, PyCSimple, PyCSimpleType}; use crate::{Py, PyRef, VirtualMachine}; +pub use crate::stdlib::ctypes::base::{PyCData, PyCSimple, PyCSimpleType}; + pub fn extend_module_nodes(vm: &VirtualMachine, module: &Py) { let ctx = &vm.ctx; PyCSimpleType::make_class(ctx); @@ -45,17 +47,17 @@ pub(crate) fn make_module(vm: &VirtualMachine) -> PyRef { #[pymodule] pub(crate) mod _ctypes { - use super::base::PyCSimple; + use super::base::{CDataObject, PyCSimple}; use crate::builtins::PyTypeRef; use crate::class::StaticType; use crate::convert::ToPyObject; use crate::function::{Either, FuncArgs, OptionalArg}; use crate::stdlib::ctypes::library; - use crate::{AsObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine}; + use crate::{AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine}; use crossbeam_utils::atomic::AtomicCell; use std::ffi::{ c_double, c_float, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint, c_ulong, - c_ulonglong, + c_ulonglong, c_ushort, }; use std::mem; use widestring::WideChar; @@ -158,6 +160,192 @@ pub(crate) mod _ctypes { } } + /// Get alignment for a simple type - for C types, alignment equals size + pub fn get_align(ty: &str) -> usize { + get_size(ty) + } + + /// Get the size of a ctypes type from its type object + #[allow(dead_code)] + pub fn get_size_from_type(cls: &PyTypeRef, vm: &VirtualMachine) -> PyResult { + // Try to get _type_ attribute for simple types + if let Ok(type_attr) = cls.as_object().get_attr("_type_", vm) + && let Ok(s) = type_attr.str(vm) + { + let s = s.to_string(); + if s.len() == 1 && SIMPLE_TYPE_CHARS.contains(s.as_str()) { + return Ok(get_size(&s)); + } + } + // Fall back to sizeof + size_of(cls.clone().into(), vm) + } + + /// Convert bytes to appropriate Python object based on ctypes type + pub fn bytes_to_pyobject( + cls: &PyTypeRef, + bytes: &[u8], + vm: &VirtualMachine, + ) -> PyResult { + // Try to get _type_ attribute + if let Ok(type_attr) = cls.as_object().get_attr("_type_", vm) + && let Ok(s) = type_attr.str(vm) + { + let ty = s.to_string(); + return match ty.as_str() { + "c" => { + // c_char - single byte + Ok(vm.ctx.new_bytes(bytes.to_vec()).into()) + } + "b" => { + // c_byte - signed char + let val = if !bytes.is_empty() { bytes[0] as i8 } else { 0 }; + Ok(vm.ctx.new_int(val).into()) + } + "B" => { + // c_ubyte - unsigned char + let val = if !bytes.is_empty() { bytes[0] } else { 0 }; + Ok(vm.ctx.new_int(val).into()) + } + "h" => { + // c_short + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_short::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "H" => { + // c_ushort + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_ushort::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "i" => { + // c_int + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_int::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "I" => { + // c_uint + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_uint::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "l" => { + // c_long + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_long::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "L" => { + // c_ulong + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_ulong::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "q" => { + // c_longlong + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_longlong::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "Q" => { + // c_ulonglong + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_ulonglong::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "f" => { + // c_float + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_float::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0.0 + }; + Ok(vm.ctx.new_float(val as f64).into()) + } + "d" | "g" => { + // c_double + const SIZE: usize = mem::size_of::(); + let val = if bytes.len() >= SIZE { + c_double::from_ne_bytes(bytes[..SIZE].try_into().expect("size checked")) + } else { + 0.0 + }; + Ok(vm.ctx.new_float(val).into()) + } + "?" => { + // c_bool + let val = !bytes.is_empty() && bytes[0] != 0; + Ok(vm.ctx.new_bool(val).into()) + } + "P" | "z" | "Z" => { + // Pointer types - return as integer address + let val = if bytes.len() >= mem::size_of::() { + const UINTPTR_LEN: usize = mem::size_of::(); + let mut arr = [0u8; UINTPTR_LEN]; + arr[..bytes.len().min(UINTPTR_LEN)] + .copy_from_slice(&bytes[..bytes.len().min(UINTPTR_LEN)]); + usize::from_ne_bytes(arr) + } else { + 0 + }; + Ok(vm.ctx.new_int(val).into()) + } + "u" => { + // c_wchar - wide character + let val = if bytes.len() >= mem::size_of::() { + let wc = if mem::size_of::() == 2 { + u16::from_ne_bytes([bytes[0], bytes[1]]) as u32 + } else { + u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) + }; + char::from_u32(wc).unwrap_or('\0') + } else { + '\0' + }; + Ok(vm.ctx.new_str(val.to_string()).into()) + } + _ => Ok(vm.ctx.none()), + }; + } + // Default: return bytes as-is + Ok(vm.ctx.new_bytes(bytes.to_vec()).into()) + } + const SIMPLE_TYPE_CHARS: &str = "cbBhHiIlLdfguzZPqQ?O"; pub fn new_simple_type( @@ -180,9 +368,14 @@ pub(crate) mod _ctypes { } else if !SIMPLE_TYPE_CHARS.contains(tp_str.as_str()) { Err(vm.new_attribute_error(format!("class must define a '_type_' attribute which must be\n a single character string containing one of {SIMPLE_TYPE_CHARS}, currently it is {tp_str}."))) } else { + let size = get_size(&tp_str); Ok(PyCSimple { _type_: tp_str, value: AtomicCell::new(vm.ctx.none()), + cdata: rustpython_common::lock::PyRwLock::new(CDataObject::from_bytes( + vec![0u8; size], + None, + )), }) } } else { @@ -193,20 +386,118 @@ pub(crate) mod _ctypes { } } + /// Get the size of a ctypes type or instance #[pyfunction(name = "sizeof")] - pub fn size_of(tp: Either, vm: &VirtualMachine) -> PyResult { - match tp { - Either::A(type_) if type_.fast_issubclass(PyCSimple::static_type()) => { - let zelf = new_simple_type(Either::B(&type_), vm)?; - Ok(get_size(zelf._type_.as_str())) + pub fn size_of(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { + use super::array::{PyCArray, PyCArrayType}; + use super::pointer::PyCPointer; + use super::structure::{PyCStructType, PyCStructure}; + use super::union::{PyCUnion, PyCUnionType}; + + // 1. Instances with stg_info + if obj.fast_isinstance(PyCArray::static_type()) { + // Get stg_info from the type + if let Some(type_obj) = obj.class().as_object().downcast_ref::() { + return Ok(type_obj.stg_info.size); } - Either::B(obj) if obj.has_attr("size_of_instances", vm)? => { - let size_of_method = obj.get_attr("size_of_instances", vm)?; - let size_of_return = size_of_method.call(vec![], vm)?; - Ok(usize::try_from_object(vm, size_of_return)?) + } + if let Some(structure) = obj.downcast_ref::() { + return Ok(structure.cdata.read().size()); + } + if obj.fast_isinstance(PyCUnion::static_type()) { + // Get stg_info from the type + if let Some(type_obj) = obj.class().as_object().downcast_ref::() { + return Ok(type_obj.stg_info.size); } - _ => Err(vm.new_type_error("this type has no size")), } + if let Some(simple) = obj.downcast_ref::() { + return Ok(simple.cdata.read().size()); + } + if obj.fast_isinstance(PyCPointer::static_type()) { + return Ok(std::mem::size_of::()); + } + + // 2. Types (metatypes with stg_info) + if let Some(array_type) = obj.downcast_ref::() { + return Ok(array_type.stg_info.size); + } + + // 3. Type objects + if let Ok(type_ref) = obj.clone().downcast::() { + // Structure types - check if metaclass is or inherits from PyCStructType + if type_ref + .class() + .fast_issubclass(PyCStructType::static_type()) + { + return calculate_struct_size(&type_ref, vm); + } + // Union types - check if metaclass is or inherits from PyCUnionType + if type_ref + .class() + .fast_issubclass(PyCUnionType::static_type()) + { + return calculate_union_size(&type_ref, vm); + } + // Simple types (c_int, c_char, etc.) + if type_ref.fast_issubclass(PyCSimple::static_type()) { + let instance = new_simple_type(Either::B(&type_ref), vm)?; + return Ok(get_size(&instance._type_)); + } + // Pointer types + if type_ref.fast_issubclass(PyCPointer::static_type()) { + return Ok(std::mem::size_of::()); + } + } + + Err(vm.new_type_error("this type has no size")) + } + + /// Calculate Structure type size from _fields_ (sum of field sizes) + fn calculate_struct_size( + cls: &crate::builtins::PyTypeRef, + vm: &VirtualMachine, + ) -> PyResult { + use crate::AsObject; + + if let Ok(fields_attr) = cls.as_object().get_attr("_fields_", vm) { + let fields: Vec = fields_attr.try_to_value(vm).unwrap_or_default(); + let mut total_size = 0usize; + + for field in fields.iter() { + if let Some(tuple) = field.downcast_ref::() + && let Some(field_type) = tuple.get(1) + { + // Recursively calculate field type size + total_size += size_of(field_type.clone(), vm)?; + } + } + return Ok(total_size); + } + Ok(0) + } + + /// Calculate Union type size from _fields_ (max field size) + fn calculate_union_size( + cls: &crate::builtins::PyTypeRef, + vm: &VirtualMachine, + ) -> PyResult { + use crate::AsObject; + + if let Ok(fields_attr) = cls.as_object().get_attr("_fields_", vm) { + let fields: Vec = fields_attr.try_to_value(vm).unwrap_or_default(); + let mut max_size = 0usize; + + for field in fields.iter() { + if let Some(tuple) = field.downcast_ref::() + && let Some(field_type) = tuple.get(1) + { + let field_size = size_of(field_type.clone(), vm)?; + max_size = max_size.max(field_size); + } + } + return Ok(max_size); + } + Ok(0) } #[cfg(windows)] @@ -367,9 +658,100 @@ pub(crate) mod _ctypes { } #[pyfunction] - fn alignment(_args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { - // TODO: RUSTPYTHON - Err(vm.new_value_error("not implemented")) + fn alignment(tp: Either, vm: &VirtualMachine) -> PyResult { + use super::array::{PyCArray, PyCArrayType}; + use super::base::PyCSimpleType; + use super::pointer::PyCPointer; + use super::structure::PyCStructure; + use super::union::PyCUnion; + + let obj = match &tp { + Either::A(t) => t.as_object(), + Either::B(o) => o.as_ref(), + }; + + // Try to get alignment from stg_info directly (for instances) + if let Some(array_type) = obj.downcast_ref::() { + return Ok(array_type.stg_info.align); + } + if obj.fast_isinstance(PyCSimple::static_type()) { + // Get stg_info from the type by reading _type_ attribute + let cls = obj.class().to_owned(); + let stg_info = PyCSimpleType::get_stg_info(&cls, vm); + return Ok(stg_info.align); + } + if obj.fast_isinstance(PyCArray::static_type()) { + // Get stg_info from the type + if let Some(type_obj) = obj.class().as_object().downcast_ref::() { + return Ok(type_obj.stg_info.align); + } + } + if obj.fast_isinstance(PyCStructure::static_type()) { + // Calculate alignment from _fields_ + let cls = obj.class(); + return alignment(Either::A(cls.to_owned()), vm); + } + if obj.fast_isinstance(PyCPointer::static_type()) { + // Pointer alignment is always pointer size + return Ok(std::mem::align_of::()); + } + if obj.fast_isinstance(PyCUnion::static_type()) { + // Calculate alignment from _fields_ + let cls = obj.class(); + return alignment(Either::A(cls.to_owned()), vm); + } + + // Get the type object to check + let type_obj: PyObjectRef = match &tp { + Either::A(t) => t.clone().into(), + Either::B(obj) => obj.class().to_owned().into(), + }; + + // For type objects, try to get alignment from _type_ attribute + if let Ok(type_attr) = type_obj.get_attr("_type_", vm) { + // Array/Pointer: _type_ is the element type (a PyType) + if let Ok(elem_type) = type_attr.clone().downcast::() { + return alignment(Either::A(elem_type), vm); + } + // Simple type: _type_ is a single character string + if let Ok(s) = type_attr.str(vm) { + let ty = s.to_string(); + if ty.len() == 1 && SIMPLE_TYPE_CHARS.contains(ty.as_str()) { + return Ok(get_align(&ty)); + } + } + } + + // Structure/Union: max alignment of fields + if let Ok(fields_attr) = type_obj.get_attr("_fields_", vm) + && let Ok(fields) = fields_attr.try_to_value::>(vm) + { + let mut max_align = 1usize; + for field in fields.iter() { + if let Some(tuple) = field.downcast_ref::() + && let Some(field_type) = tuple.get(1) + { + let align = + if let Ok(ft) = field_type.clone().downcast::() { + alignment(Either::A(ft), vm).unwrap_or(1) + } else { + 1 + }; + max_align = max_align.max(align); + } + } + return Ok(max_align); + } + + // For instances, delegate to their class + if let Either::B(obj) = &tp + && !obj.class().is(vm.ctx.types.type_type.as_ref()) + { + return alignment(Either::A(obj.class().to_owned()), vm); + } + + // No alignment info found + Err(vm.new_type_error("no alignment info")) } #[pyfunction] @@ -439,4 +821,53 @@ pub(crate) mod _ctypes { // todo!("Implement _cast_addr") 0 } + + #[pyfunction(name = "_cast")] + pub fn pycfunction_cast( + obj: PyObjectRef, + _obj2: PyObjectRef, + ctype: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + use super::array::PyCArray; + use super::base::PyCData; + use super::pointer::PyCPointer; + use crate::class::StaticType; + + // Python signature: _cast(obj, obj, ctype) + // Python passes the same object twice (obj and _obj2 are the same) + // We ignore _obj2 as it's redundant + + // Check if this is a pointer type (has _type_ attribute) + if ctype.get_attr("_type_", vm).is_err() { + return Err(vm.new_type_error("cast() argument 2 must be a pointer type".to_string())); + } + + // Create an instance of the target pointer type with no arguments + let result = ctype.call((), vm)?; + + // Get the pointer value from the source object + // If obj is a CData instance (including arrays), use the object itself + // If obj is an integer, use it directly as the pointer value + let ptr_value: PyObjectRef = if obj.fast_isinstance(PyCData::static_type()) + || obj.fast_isinstance(PyCArray::static_type()) + || obj.fast_isinstance(PyCPointer::static_type()) + { + // For CData objects (including arrays and pointers), store the object itself + obj.clone() + } else if let Ok(int_val) = obj.try_int(vm) { + // For integers, treat as pointer address + vm.ctx.new_int(int_val.as_bigint().clone()).into() + } else { + return Err(vm.new_type_error(format!( + "cast() argument 1 must be a ctypes instance or an integer, not {}", + obj.class().name() + ))); + }; + + // Set the contents of the pointer by setting the attribute + result.set_attr("contents", ptr_value, vm)?; + + Ok(result) + } } diff --git a/crates/vm/src/stdlib/ctypes/array.rs b/crates/vm/src/stdlib/ctypes/array.rs index 73217d7a6a..62d714329e 100644 --- a/crates/vm/src/stdlib/ctypes/array.rs +++ b/crates/vm/src/stdlib/ctypes/array.rs @@ -2,8 +2,12 @@ use crate::atomic_func; use crate::builtins::{PyBytes, PyInt}; use crate::convert::ToPyObject; use crate::function::FuncArgs; -use crate::protocol::{PyNumberMethods, PySequenceMethods}; -use crate::types::{AsNumber, AsSequence, Callable}; +use crate::protocol::{ + BufferDescriptor, BufferMethods, PyBuffer, PyNumberMethods, PySequenceMethods, +}; +use crate::stdlib::ctypes::base::CDataObject; +use crate::stdlib::ctypes::util::StgInfo; +use crate::types::{AsBuffer, AsNumber, AsSequence, Callable}; use crate::{AsObject, Py, PyObjectRef, PyPayload}; use crate::{ PyResult, VirtualMachine, @@ -19,13 +23,17 @@ use rustpython_vm::stdlib::ctypes::base::PyCData; #[pyclass(name = "PyCArrayType", base = PyType, module = "_ctypes")] #[derive(PyPayload)] pub struct PyCArrayType { - pub(super) inner: PyCArray, + pub(super) stg_info: StgInfo, + pub(super) typ: PyRwLock, + pub(super) length: AtomicCell, + pub(super) element_size: AtomicCell, } impl std::fmt::Debug for PyCArrayType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PyCArrayType") - .field("inner", &self.inner) + .field("typ", &self.typ) + .field("length", &self.length) .finish() } } @@ -34,9 +42,9 @@ impl Callable for PyCArrayType { type Args = FuncArgs; fn call(zelf: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { // Create an instance of the array - let element_type = zelf.inner.typ.read().clone(); - let length = zelf.inner.length.load(); - let element_size = zelf.inner.element_size.load(); + let element_type = zelf.typ.read().clone(); + let length = zelf.length.load(); + let element_size = zelf.element_size.load(); let total_size = element_size * length; let mut buffer = vec![0u8; total_size]; @@ -58,7 +66,7 @@ impl Callable for PyCArrayType { typ: PyRwLock::new(element_type), length: AtomicCell::new(length), element_size: AtomicCell::new(element_size), - buffer: PyRwLock::new(buffer), + cdata: PyRwLock::new(CDataObject::from_bytes(buffer, None)), } .into_pyobject(vm)) } @@ -75,13 +83,13 @@ impl Constructor for PyCArrayType { #[pyclass(flags(IMMUTABLETYPE), with(Callable, Constructor, AsNumber))] impl PyCArrayType { #[pygetset(name = "_type_")] - fn typ(&self) -> PyTypeRef { - self.inner.typ.read().clone() + fn typ(&self) -> PyObjectRef { + self.typ.read().clone() } #[pygetset(name = "_length_")] fn length(&self) -> usize { - self.inner.length.load() + self.length.load() } #[pymethod] @@ -92,28 +100,106 @@ impl PyCArrayType { // Create a nested array type: (inner_type * inner_length) * n // The new array has n elements, each element is the current array type // e.g., (c_int * 5) * 3 = Array of 3 elements, each is (c_int * 5) - let inner_length = zelf.inner.length.load(); - let inner_element_size = zelf.inner.element_size.load(); + let inner_length = zelf.length.load(); + let inner_element_size = zelf.element_size.load(); // The element type of the new array is the current array type itself - let obj_ref: PyObjectRef = zelf.to_owned().into(); - let current_array_type = obj_ref - .downcast::() - .expect("PyCArrayType should be a PyType"); + let current_array_type: PyObjectRef = zelf.as_object().to_owned(); // Element size is the total size of the inner array let new_element_size = inner_length * inner_element_size; + let total_size = new_element_size * (n as usize); + let stg_info = StgInfo::new(total_size, inner_element_size); Ok(PyCArrayType { - inner: PyCArray { - typ: PyRwLock::new(current_array_type), - length: AtomicCell::new(n as usize), - element_size: AtomicCell::new(new_element_size), - buffer: PyRwLock::new(vec![]), - }, + stg_info, + typ: PyRwLock::new(current_array_type), + length: AtomicCell::new(n as usize), + element_size: AtomicCell::new(new_element_size), } .to_pyobject(vm)) } + + #[pyclassmethod] + fn in_dll( + zelf: &Py, + dll: PyObjectRef, + name: crate::builtins::PyStrRef, + vm: &VirtualMachine, + ) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + use libloading::Symbol; + + // Get the library handle from dll object + let handle = if let Ok(int_handle) = dll.try_int(vm) { + // dll is an integer handle + int_handle + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + } else { + // dll is a CDLL/PyDLL/WinDLL object with _handle attribute + dll.get_attr("_handle", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + }; + + // Get the library from cache + let library_cache = crate::stdlib::ctypes::library::libcache().read(); + let library = library_cache + .get_lib(handle) + .ok_or_else(|| vm.new_attribute_error("Library not found".to_owned()))?; + + // Get symbol address from library + let symbol_name = format!("{}\0", name.as_str()); + let inner_lib = library.lib.lock(); + + let symbol_address = if let Some(lib) = &*inner_lib { + unsafe { + // Try to get the symbol from the library + let symbol: Symbol<'_, *mut u8> = lib.get(symbol_name.as_bytes()).map_err(|e| { + vm.new_attribute_error(format!("{}: symbol '{}' not found", e, name.as_str())) + })?; + *symbol as usize + } + } else { + return Err(vm.new_attribute_error("Library is closed".to_owned())); + }; + + // Get size from the array type + let element_type = zelf.typ.read().clone(); + let length = zelf.length.load(); + let element_size = size_of(element_type.clone(), vm)?; + let total_size = element_size * length; + + // Read data from symbol address + let data = if symbol_address != 0 && total_size > 0 { + unsafe { + let ptr = symbol_address as *const u8; + std::slice::from_raw_parts(ptr, total_size).to_vec() + } + } else { + vec![0; total_size] + }; + + // Create instance + let instance = PyCArray { + typ: PyRwLock::new(element_type), + length: AtomicCell::new(length), + element_size: AtomicCell::new(element_size), + cdata: PyRwLock::new(CDataObject::from_bytes(data, None)), + } + .into_pyobject(vm); + + // Store base reference to keep dll alive + if let Ok(array_ref) = instance.clone().downcast::() { + array_ref.cdata.write().base = Some(dll); + } + + Ok(instance) + } } impl AsNumber for PyCArrayType { @@ -144,10 +230,11 @@ impl AsNumber for PyCArrayType { )] #[derive(PyPayload)] pub struct PyCArray { - pub(super) typ: PyRwLock, + /// Element type - can be a simple type (c_int) or an array type (c_int * 5) + pub(super) typ: PyRwLock, pub(super) length: AtomicCell, pub(super) element_size: AtomicCell, - pub(super) buffer: PyRwLock>, + pub(super) cdata: PyRwLock, } impl std::fmt::Debug for PyCArray { @@ -207,15 +294,11 @@ impl Constructor for PyCArray { } } - let element_type_ref = element_type - .downcast::() - .unwrap_or_else(|_| vm.ctx.types.object_type.to_owned()); - PyCArray { - typ: PyRwLock::new(element_type_ref), + typ: PyRwLock::new(element_type), length: AtomicCell::new(length), element_size: AtomicCell::new(element_size), - buffer: PyRwLock::new(buffer), + cdata: PyRwLock::new(CDataObject::from_bytes(buffer, None)), } .into_ref_with_type(vm, cls) .map(Into::into) @@ -243,8 +326,16 @@ impl AsSequence for PyCArray { } } -#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor, AsSequence))] +#[pyclass( + flags(BASETYPE, IMMUTABLETYPE), + with(Constructor, AsSequence, AsBuffer) +)] impl PyCArray { + #[pygetset] + fn _objects(&self) -> Option { + self.cdata.read().objects.clone() + } + fn int_to_bytes(i: &malachite_bigint::BigInt, size: usize) -> Vec { match size { 1 => vec![i.to_i8().unwrap_or(0) as u8], @@ -285,7 +376,7 @@ impl PyCArray { let index = index as usize; let element_size = zelf.element_size.load(); let offset = index * element_size; - let buffer = zelf.buffer.read(); + let buffer = zelf.cdata.read().buffer.clone(); if offset + element_size <= buffer.len() { let bytes = &buffer[offset..offset + element_size]; Ok(Self::bytes_to_int(bytes, element_size, vm)) @@ -312,9 +403,9 @@ impl PyCArray { let int_val = value.try_int(vm)?; let bytes = Self::int_to_bytes(int_val.as_bigint(), element_size); - let mut buffer = zelf.buffer.write(); - if offset + element_size <= buffer.len() { - buffer[offset..offset + element_size].copy_from_slice(&bytes); + let mut cdata = zelf.cdata.write(); + if offset + element_size <= cdata.buffer.len() { + cdata.buffer[offset..offset + element_size].copy_from_slice(&bytes); } Ok(()) } @@ -354,7 +445,7 @@ impl PyCArray { } #[pygetset(name = "_type_")] - fn typ(&self) -> PyTypeRef { + fn typ(&self) -> PyObjectRef { self.typ.read().clone() } @@ -366,48 +457,337 @@ impl PyCArray { #[pygetset] fn value(&self, vm: &VirtualMachine) -> PyObjectRef { // Return bytes representation of the buffer - let buffer = self.buffer.read(); + let buffer = self.cdata.read().buffer.clone(); vm.ctx.new_bytes(buffer.clone()).into() } #[pygetset(setter)] fn set_value(&self, value: PyObjectRef, _vm: &VirtualMachine) -> PyResult<()> { if let Some(bytes) = value.downcast_ref::() { - let mut buffer = self.buffer.write(); + let mut cdata = self.cdata.write(); let src = bytes.as_bytes(); - let len = std::cmp::min(src.len(), buffer.len()); - buffer[..len].copy_from_slice(&src[..len]); + let len = std::cmp::min(src.len(), cdata.buffer.len()); + cdata.buffer[..len].copy_from_slice(&src[..len]); } Ok(()) } #[pygetset] fn raw(&self, vm: &VirtualMachine) -> PyObjectRef { - let buffer = self.buffer.read(); - vm.ctx.new_bytes(buffer.clone()).into() + let cdata = self.cdata.read(); + vm.ctx.new_bytes(cdata.buffer.clone()).into() } #[pygetset(setter)] fn set_raw(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { if let Some(bytes) = value.downcast_ref::() { - let mut buffer = self.buffer.write(); + let mut cdata = self.cdata.write(); let src = bytes.as_bytes(); - let len = std::cmp::min(src.len(), buffer.len()); - buffer[..len].copy_from_slice(&src[..len]); + let len = std::cmp::min(src.len(), cdata.buffer.len()); + cdata.buffer[..len].copy_from_slice(&src[..len]); Ok(()) } else { Err(vm.new_type_error("expected bytes".to_owned())) } } + + #[pyclassmethod] + fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Create instance with data from address + if address == 0 || size == 0 { + return Err(vm.new_value_error("NULL pointer access".to_owned())); + } + unsafe { + let ptr = address as *const u8; + let bytes = std::slice::from_raw_parts(ptr, size); + // Get element type and length from cls + let element_type = cls.as_object().get_attr("_type_", vm)?; + let element_type: PyTypeRef = element_type + .downcast() + .map_err(|_| vm.new_type_error("_type_ must be a type".to_owned()))?; + let length = cls + .as_object() + .get_attr("_length_", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .unwrap_or(0); + let element_size = if length > 0 { size / length } else { 0 }; + + Ok(PyCArray { + typ: PyRwLock::new(element_type.into()), + length: AtomicCell::new(length), + element_size: AtomicCell::new(element_size), + cdata: PyRwLock::new(CDataObject::from_bytes(bytes.to_vec(), None)), + } + .into_pyobject(vm)) + } + } + + #[pyclassmethod] + fn from_buffer( + cls: PyTypeRef, + source: PyObjectRef, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use crate::TryFromObject; + use crate::protocol::PyBuffer; + use crate::stdlib::ctypes::_ctypes::size_of; + + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + // Get buffer from source + let buffer = PyBuffer::try_from_object(vm, source.clone())?; + + // Check if buffer is writable + if buffer.desc.readonly { + return Err(vm.new_type_error("underlying buffer is not writable".to_owned())); + } + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Check if buffer is large enough + let buffer_len = buffer.desc.len; + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Read bytes from buffer at offset + let bytes = buffer.obj_bytes(); + let data = &bytes[offset..offset + size]; + + // Get element type and length from cls + let element_type = cls.as_object().get_attr("_type_", vm)?; + let element_type: PyTypeRef = element_type + .downcast() + .map_err(|_| vm.new_type_error("_type_ must be a type".to_owned()))?; + let length = cls + .as_object() + .get_attr("_length_", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .unwrap_or(0); + let element_size = if length > 0 { size / length } else { 0 }; + + Ok(PyCArray { + typ: PyRwLock::new(element_type.into()), + length: AtomicCell::new(length), + element_size: AtomicCell::new(element_size), + cdata: PyRwLock::new(CDataObject::from_bytes( + data.to_vec(), + Some(buffer.obj.clone()), + )), + } + .into_pyobject(vm)) + } + + #[pyclassmethod] + fn from_buffer_copy( + cls: PyTypeRef, + source: crate::function::ArgBytesLike, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Borrow bytes from source + let source_bytes = source.borrow_buf(); + let buffer_len = source_bytes.len(); + + // Check if buffer is large enough + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Copy bytes from buffer at offset + let data = &source_bytes[offset..offset + size]; + + // Get element type and length from cls + let element_type = cls.as_object().get_attr("_type_", vm)?; + let element_type: PyTypeRef = element_type + .downcast() + .map_err(|_| vm.new_type_error("_type_ must be a type".to_owned()))?; + let length = cls + .as_object() + .get_attr("_length_", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .unwrap_or(0); + let element_size = if length > 0 { size / length } else { 0 }; + + Ok(PyCArray { + typ: PyRwLock::new(element_type.into()), + length: AtomicCell::new(length), + element_size: AtomicCell::new(element_size), + cdata: PyRwLock::new(CDataObject::from_bytes(data.to_vec(), None)), + } + .into_pyobject(vm)) + } + + #[pyclassmethod] + fn in_dll( + cls: PyTypeRef, + dll: PyObjectRef, + name: crate::builtins::PyStrRef, + vm: &VirtualMachine, + ) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + use libloading::Symbol; + + // Get the library handle from dll object + let handle = if let Ok(int_handle) = dll.try_int(vm) { + // dll is an integer handle + int_handle + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + } else { + // dll is a CDLL/PyDLL/WinDLL object with _handle attribute + dll.get_attr("_handle", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + }; + + // Get the library from cache + let library_cache = crate::stdlib::ctypes::library::libcache().read(); + let library = library_cache + .get_lib(handle) + .ok_or_else(|| vm.new_attribute_error("Library not found".to_owned()))?; + + // Get symbol address from library + let symbol_name = format!("{}\0", name.as_str()); + let inner_lib = library.lib.lock(); + + let symbol_address = if let Some(lib) = &*inner_lib { + unsafe { + // Try to get the symbol from the library + let symbol: Symbol<'_, *mut u8> = lib.get(symbol_name.as_bytes()).map_err(|e| { + vm.new_attribute_error(format!("{}: symbol '{}' not found", e, name.as_str())) + })?; + *symbol as usize + } + } else { + return Err(vm.new_attribute_error("Library is closed".to_owned())); + }; + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Read data from symbol address + let data = if symbol_address != 0 && size > 0 { + unsafe { + let ptr = symbol_address as *const u8; + std::slice::from_raw_parts(ptr, size).to_vec() + } + } else { + vec![0; size] + }; + + // Get element type and length from cls + let element_type = cls.as_object().get_attr("_type_", vm)?; + let element_type: PyTypeRef = element_type + .downcast() + .map_err(|_| vm.new_type_error("_type_ must be a type".to_owned()))?; + let length = cls + .as_object() + .get_attr("_length_", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .unwrap_or(0); + let element_size = if length > 0 { size / length } else { 0 }; + + // Create instance + let instance = PyCArray { + typ: PyRwLock::new(element_type.into()), + length: AtomicCell::new(length), + element_size: AtomicCell::new(element_size), + cdata: PyRwLock::new(CDataObject::from_bytes(data, None)), + } + .into_pyobject(vm); + + // Store base reference to keep dll alive + if let Ok(array_ref) = instance.clone().downcast::() { + array_ref.cdata.write().base = Some(dll); + } + + Ok(instance) + } } impl PyCArray { #[allow(unused)] pub fn to_arg(&self, _vm: &VirtualMachine) -> PyResult { - // TODO: This needs a different approach to ensure buffer lifetime - // The buffer must outlive the Arg returned here - let buffer = self.buffer.read(); - let ptr = buffer.as_ptr(); - Ok(libffi::middle::Arg::new(&ptr)) + let cdata = self.cdata.read(); + Ok(libffi::middle::Arg::new(&cdata.buffer)) + } +} + +static ARRAY_BUFFER_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| { + rustpython_common::lock::PyMappedRwLockReadGuard::map( + rustpython_common::lock::PyRwLockReadGuard::map( + buffer.obj_as::().cdata.read(), + |x: &CDataObject| x, + ), + |x: &CDataObject| x.buffer.as_slice(), + ) + .into() + }, + obj_bytes_mut: |buffer| { + rustpython_common::lock::PyMappedRwLockWriteGuard::map( + rustpython_common::lock::PyRwLockWriteGuard::map( + buffer.obj_as::().cdata.write(), + |x: &mut CDataObject| x, + ), + |x: &mut CDataObject| x.buffer.as_mut_slice(), + ) + .into() + }, + release: |_| {}, + retain: |_| {}, +}; + +impl AsBuffer for PyCArray { + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let buffer_len = zelf.cdata.read().buffer.len(); + let buf = PyBuffer::new( + zelf.to_owned().into(), + BufferDescriptor::simple(buffer_len, false), // readonly=false for ctypes + &ARRAY_BUFFER_METHODS, + ); + Ok(buf) } } diff --git a/crates/vm/src/stdlib/ctypes/base.rs b/crates/vm/src/stdlib/ctypes/base.rs index 73d97a2593..5e2deeb560 100644 --- a/crates/vm/src/stdlib/ctypes/base.rs +++ b/crates/vm/src/stdlib/ctypes/base.rs @@ -1,17 +1,27 @@ -use super::array::{PyCArray, PyCArrayType}; -use crate::builtins::PyType; -use crate::builtins::{PyBytes, PyFloat, PyInt, PyNone, PyStr, PyTypeRef}; +use super::_ctypes::bytes_to_pyobject; +use super::array::PyCArrayType; +use super::util::StgInfo; +use crate::builtins::{PyBytes, PyFloat, PyInt, PyNone, PyStr, PyStrRef, PyType, PyTypeRef}; use crate::convert::ToPyObject; -use crate::function::{Either, OptionalArg}; -use crate::protocol::PyNumberMethods; +use crate::function::{ArgBytesLike, Either, OptionalArg}; +use crate::protocol::{BufferDescriptor, BufferMethods, PyBuffer, PyNumberMethods}; use crate::stdlib::ctypes::_ctypes::new_simple_type; -use crate::types::{AsNumber, Constructor}; +use crate::types::{AsBuffer, AsNumber, Constructor}; use crate::{AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine}; use crossbeam_utils::atomic::AtomicCell; use num_traits::ToPrimitive; use rustpython_common::lock::PyRwLock; +use std::ffi::{c_uint, c_ulong, c_ulonglong, c_ushort}; use std::fmt::Debug; +/// Get the type code string from a ctypes type (e.g., "i" for c_int) +pub fn get_type_code(cls: &PyTypeRef, vm: &VirtualMachine) -> Option { + cls.as_object() + .get_attr("_type_", vm) + .ok() + .and_then(|t| t.downcast_ref::().map(|s| s.to_string())) +} + pub fn ffi_type_from_str(_type_: &str) -> Option { match _type_ { "c" => Some(libffi::middle::Type::u8()), @@ -91,7 +101,10 @@ fn set_primitive(_type_: &str, value: &PyObjectRef, vm: &VirtualMachine) -> PyRe } } "f" | "d" | "g" => { - if value.clone().downcast_exact::(vm).is_ok() { + // float allows int + if value.clone().downcast_exact::(vm).is_ok() + || value.clone().downcast_exact::(vm).is_ok() + { Ok(value.clone()) } else { Err(vm.new_type_error(format!("must be real number, not {}", value.class().name()))) @@ -102,7 +115,8 @@ fn set_primitive(_type_: &str, value: &PyObjectRef, vm: &VirtualMachine) -> PyRe )), "B" => { if value.clone().downcast_exact::(vm).is_ok() { - Ok(vm.new_pyobj(u8::try_from_object(vm, value.clone())?)) + // Store as-is, conversion to unsigned happens in the getter + Ok(value.clone()) } else { Err(vm.new_type_error(format!("int expected instead of {}", value.class().name()))) } @@ -142,28 +156,102 @@ fn set_primitive(_type_: &str, value: &PyObjectRef, vm: &VirtualMachine) -> PyRe } } -pub struct RawBuffer { +/// Common data object for all ctypes types +#[derive(Debug, Clone)] +pub struct CDataObject { + /// pointer to memory block (b_ptr + b_size) + pub buffer: Vec, + /// pointer to base object or None (b_base) + #[allow(dead_code)] + pub base: Option, + /// index into base's b_objects list (b_index) #[allow(dead_code)] - pub inner: Box<[u8]>, + pub index: usize, + /// dictionary of references we need to keep (b_objects) + pub objects: Option, +} + +impl CDataObject { + /// Create from StgInfo (PyCData_MallocBuffer pattern) + pub fn from_stg_info(stg_info: &StgInfo) -> Self { + CDataObject { + buffer: vec![0u8; stg_info.size], + base: None, + index: 0, + objects: None, + } + } + + /// Create from existing bytes (copies data) + pub fn from_bytes(data: Vec, objects: Option) -> Self { + CDataObject { + buffer: data, + base: None, + index: 0, + objects, + } + } + + /// Create from base object (copies data from base's buffer at offset) #[allow(dead_code)] - pub size: usize, + pub fn from_base( + base: PyObjectRef, + _offset: usize, + size: usize, + index: usize, + objects: Option, + ) -> Self { + CDataObject { + buffer: vec![0u8; size], + base: Some(base), + index, + objects, + } + } + + #[inline] + pub fn size(&self) -> usize { + self.buffer.len() + } } #[pyclass(name = "_CData", module = "_ctypes")] +#[derive(Debug, PyPayload)] pub struct PyCData { - _objects: AtomicCell>, - _buffer: PyRwLock, + pub cdata: PyRwLock, } -#[pyclass] -impl PyCData {} +#[pyclass(flags(BASETYPE))] +impl PyCData { + #[pygetset] + fn _objects(&self) -> Option { + self.cdata.read().objects.clone() + } +} #[pyclass(module = "_ctypes", name = "PyCSimpleType", base = PyType)] -#[derive(Debug, PyPayload)] -pub struct PyCSimpleType {} +#[derive(Debug, PyPayload, Default)] +pub struct PyCSimpleType { + #[allow(dead_code)] + pub stg_info: StgInfo, +} #[pyclass(flags(BASETYPE), with(AsNumber))] impl PyCSimpleType { + /// Get stg_info for a simple type by reading _type_ attribute + pub fn get_stg_info(cls: &PyTypeRef, vm: &VirtualMachine) -> StgInfo { + if let Ok(type_attr) = cls.as_object().get_attr("_type_", vm) + && let Ok(type_str) = type_attr.str(vm) + { + let tp_str = type_str.to_string(); + if tp_str.len() == 1 { + let size = super::_ctypes::get_size(&tp_str); + let align = super::_ctypes::get_align(&tp_str); + return StgInfo::new(size, align); + } + } + StgInfo::default() + } #[allow(clippy::new_ret_no_self)] #[pymethod] fn new(cls: PyTypeRef, _: OptionalArg, vm: &VirtualMachine) -> PyResult { @@ -176,17 +264,122 @@ impl PyCSimpleType { #[pyclassmethod] fn from_param(cls: PyTypeRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult { - // If the value is already an instance of the requested type, return it + // 1. If the value is already an instance of the requested type, return it if value.fast_isinstance(&cls) { return Ok(value); } - // Check for _as_parameter_ attribute - let Ok(as_parameter) = value.get_attr("_as_parameter_", vm) else { - return Err(vm.new_type_error("wrong type")); - }; + // 2. Get the type code to determine conversion rules + let type_code = get_type_code(&cls, vm); + + // 3. Handle None for pointer types (c_char_p, c_wchar_p, c_void_p) + if vm.is_none(&value) && matches!(type_code.as_deref(), Some("z") | Some("Z") | Some("P")) { + return Ok(value); + } + + // 4. Try to convert value based on type code + match type_code.as_deref() { + // Integer types: accept integers + Some("b" | "B" | "h" | "H" | "i" | "I" | "l" | "L" | "q" | "Q") => { + if value.try_int(vm).is_ok() { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + } + // Float types: accept numbers + Some("f" | "d" | "g") => { + if value.try_float(vm).is_ok() || value.try_int(vm).is_ok() { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + } + // c_char: 1 byte character + Some("c") => { + if let Some(bytes) = value.downcast_ref::() + && bytes.len() == 1 + { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + if let Ok(int_val) = value.try_int(vm) + && int_val.as_bigint().to_u8().is_some() + { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + return Err(vm.new_type_error( + "one character bytes, bytearray or integer expected".to_string(), + )); + } + // c_wchar: 1 unicode character + Some("u") => { + if let Some(s) = value.downcast_ref::() + && s.as_str().chars().count() == 1 + { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + return Err(vm.new_type_error("one character unicode string expected".to_string())); + } + // c_char_p: bytes pointer + Some("z") => { + if value.downcast_ref::().is_some() { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + } + // c_wchar_p: unicode pointer + Some("Z") => { + if value.downcast_ref::().is_some() { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + } + // c_void_p: most flexible - accepts int, bytes, str + Some("P") => { + if value.try_int(vm).is_ok() + || value.downcast_ref::().is_some() + || value.downcast_ref::().is_some() + { + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(value.clone()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + } + // c_bool + Some("?") => { + let bool_val = value.is_true(vm)?; + let simple = new_simple_type(Either::B(&cls), vm)?; + simple.value.store(vm.ctx.new_bool(bool_val).into()); + return simple.into_ref_with_type(vm, cls.clone()).map(Into::into); + } + _ => {} + } - PyCSimpleType::from_param(cls, as_parameter, vm) + // 5. Check for _as_parameter_ attribute + if let Ok(as_parameter) = value.get_attr("_as_parameter_", vm) { + return PyCSimpleType::from_param(cls, as_parameter, vm); + } + + // 6. Type-specific error messages + match type_code.as_deref() { + Some("z") => Err(vm.new_type_error(format!( + "'{}' object cannot be interpreted as ctypes.c_char_p", + value.class().name() + ))), + Some("Z") => Err(vm.new_type_error(format!( + "'{}' object cannot be interpreted as ctypes.c_wchar_p", + value.class().name() + ))), + _ => Err(vm.new_type_error("wrong type".to_string())), + } } #[pymethod] @@ -227,6 +420,7 @@ impl AsNumber for PyCSimpleType { pub struct PyCSimple { pub _type_: String, pub value: AtomicCell, + pub cdata: PyRwLock, } impl Debug for PyCSimple { @@ -237,6 +431,182 @@ impl Debug for PyCSimple { } } +fn value_to_bytes_endian( + _type_: &str, + value: &PyObjectRef, + swapped: bool, + vm: &VirtualMachine, +) -> Vec { + // Helper macro for endian conversion + macro_rules! to_bytes { + ($val:expr) => { + if swapped { + // Use opposite endianness + #[cfg(target_endian = "little")] + { + $val.to_be_bytes().to_vec() + } + #[cfg(target_endian = "big")] + { + $val.to_le_bytes().to_vec() + } + } else { + $val.to_ne_bytes().to_vec() + } + }; + } + + match _type_ { + "c" => { + // c_char - single byte + if let Some(bytes) = value.downcast_ref::() + && !bytes.is_empty() + { + return vec![bytes.as_bytes()[0]]; + } + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_u8() + { + return vec![v]; + } + vec![0] + } + "u" => { + // c_wchar - 4 bytes (wchar_t on most platforms) + if let Ok(s) = value.str(vm) + && let Some(c) = s.as_str().chars().next() + { + return to_bytes!(c as u32); + } + vec![0; 4] + } + "b" => { + // c_byte - signed char (1 byte) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_i8() + { + return vec![v as u8]; + } + vec![0] + } + "B" => { + // c_ubyte - unsigned char (1 byte) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_u8() + { + return vec![v]; + } + vec![0] + } + "h" => { + // c_short (2 bytes) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_i16() + { + return to_bytes!(v); + } + vec![0; 2] + } + "H" => { + // c_ushort (2 bytes) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_u16() + { + return to_bytes!(v); + } + vec![0; 2] + } + "i" => { + // c_int (4 bytes) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_i32() + { + return to_bytes!(v); + } + vec![0; 4] + } + "I" => { + // c_uint (4 bytes) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_u32() + { + return to_bytes!(v); + } + vec![0; 4] + } + "l" => { + // c_long (platform dependent) + if let Ok(int_val) = value.try_to_value::(vm) { + return to_bytes!(int_val); + } + const SIZE: usize = std::mem::size_of::(); + vec![0; SIZE] + } + "L" => { + // c_ulong (platform dependent) + if let Ok(int_val) = value.try_to_value::(vm) { + return to_bytes!(int_val); + } + const SIZE: usize = std::mem::size_of::(); + vec![0; SIZE] + } + "q" => { + // c_longlong (8 bytes) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_i64() + { + return to_bytes!(v); + } + vec![0; 8] + } + "Q" => { + // c_ulonglong (8 bytes) + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_u64() + { + return to_bytes!(v); + } + vec![0; 8] + } + "f" => { + // c_float (4 bytes) - int도 허용 + if let Ok(float_val) = value.try_float(vm) { + return to_bytes!(float_val.to_f64() as f32); + } + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_f64() + { + return to_bytes!(v as f32); + } + vec![0; 4] + } + "d" | "g" => { + // c_double (8 bytes) - int도 허용 + if let Ok(float_val) = value.try_float(vm) { + return to_bytes!(float_val.to_f64()); + } + if let Ok(int_val) = value.try_int(vm) + && let Some(v) = int_val.as_bigint().to_f64() + { + return to_bytes!(v); + } + vec![0; 8] + } + "?" => { + // c_bool (1 byte) + if let Ok(b) = value.clone().try_to_bool(vm) { + return vec![if b { 1 } else { 0 }]; + } + vec![0] + } + "P" | "z" | "Z" => { + // Pointer types (platform pointer size) + vec![0; std::mem::size_of::()] + } + _ => vec![0], + } +} + impl Constructor for PyCSimple { type Args = (OptionalArg,); @@ -272,31 +642,99 @@ impl Constructor for PyCSimple { _ => vm.ctx.none(), // "z" | "Z" | "P" } }; + + // Check if this is a swapped endian type + let swapped = cls + .as_object() + .get_attr("_swappedbytes_", vm) + .map(|v| v.is_true(vm).unwrap_or(false)) + .unwrap_or(false); + + let buffer = value_to_bytes_endian(&_type_, &value, swapped, vm); PyCSimple { _type_, value: AtomicCell::new(value), + cdata: PyRwLock::new(CDataObject::from_bytes(buffer, None)), } .into_ref_with_type(vm, cls) .map(Into::into) } } -#[pyclass(flags(BASETYPE), with(Constructor))] +#[pyclass(flags(BASETYPE), with(Constructor, AsBuffer))] impl PyCSimple { + #[pygetset] + fn _objects(&self) -> Option { + self.cdata.read().objects.clone() + } + #[pygetset(name = "value")] pub fn value(instance: PyObjectRef, vm: &VirtualMachine) -> PyResult { let zelf: &Py = instance .downcast_ref() .ok_or_else(|| vm.new_type_error("cannot get value of instance"))?; - Ok(unsafe { (*zelf.value.as_ptr()).clone() }) + let raw_value = unsafe { (*zelf.value.as_ptr()).clone() }; + + // Convert to unsigned if needed for unsigned types + match zelf._type_.as_str() { + "B" | "H" | "I" | "L" | "Q" => { + if let Ok(int_val) = raw_value.try_int(vm) { + let n = int_val.as_bigint(); + // Use platform-specific C types for correct unsigned conversion + match zelf._type_.as_str() { + "B" => { + if let Some(v) = n.to_i64() { + return Ok(vm.ctx.new_int((v as u8) as u64).into()); + } + } + "H" => { + if let Some(v) = n.to_i64() { + return Ok(vm.ctx.new_int((v as c_ushort) as u64).into()); + } + } + "I" => { + if let Some(v) = n.to_i64() { + return Ok(vm.ctx.new_int((v as c_uint) as u64).into()); + } + } + "L" => { + if let Some(v) = n.to_i128() { + return Ok(vm.ctx.new_int(v as c_ulong).into()); + } + } + "Q" => { + if let Some(v) = n.to_i128() { + return Ok(vm.ctx.new_int(v as c_ulonglong).into()); + } + } + _ => {} + }; + } + Ok(raw_value) + } + _ => Ok(raw_value), + } } #[pygetset(name = "value", setter)] fn set_value(instance: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { let zelf: PyRef = instance + .clone() .downcast() .map_err(|_| vm.new_type_error("cannot set value of instance"))?; let content = set_primitive(zelf._type_.as_str(), &value, vm)?; + + // Check if this is a swapped endian type + let swapped = instance + .class() + .as_object() + .get_attr("_swappedbytes_", vm) + .map(|v| v.is_true(vm).unwrap_or(false)) + .unwrap_or(false); + + // Update buffer when value changes + let buffer_bytes = value_to_bytes_endian(&zelf._type_, &content, swapped, vm); + zelf.cdata.write().buffer = buffer_bytes; zelf.value.store(content); Ok(()) } @@ -322,16 +760,214 @@ impl PyCSimple { } else { std::mem::size_of::() }; + let total_size = element_size * (n as usize); + let stg_info = super::util::StgInfo::new(total_size, element_size); Ok(PyCArrayType { - inner: PyCArray { - typ: PyRwLock::new(cls), - length: AtomicCell::new(n as usize), - element_size: AtomicCell::new(element_size), - buffer: PyRwLock::new(vec![]), - }, + stg_info, + typ: PyRwLock::new(cls.clone().into()), + length: AtomicCell::new(n as usize), + element_size: AtomicCell::new(element_size), } .to_pyobject(vm)) } + + #[pyclassmethod] + fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult { + use super::_ctypes::get_size; + // Get _type_ attribute directly + let type_attr = cls + .as_object() + .get_attr("_type_", vm) + .map_err(|_| vm.new_type_error(format!("'{}' has no _type_ attribute", cls.name())))?; + let type_str = type_attr.str(vm)?.to_string(); + let size = get_size(&type_str); + + // Create instance with value read from address + let value = if address != 0 && size > 0 { + // Safety: This is inherently unsafe - reading from arbitrary memory address + unsafe { + let ptr = address as *const u8; + let bytes = std::slice::from_raw_parts(ptr, size); + // Convert bytes to appropriate Python value based on type + bytes_to_pyobject(&cls, bytes, vm)? + } + } else { + vm.ctx.none() + }; + + // Create instance using the type's constructor + let instance = PyCSimple::py_new(cls.clone(), (OptionalArg::Present(value),), vm)?; + Ok(instance) + } + + #[pyclassmethod] + fn from_buffer( + cls: PyTypeRef, + source: PyObjectRef, + offset: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use super::_ctypes::get_size; + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + // Get buffer from source + let buffer = PyBuffer::try_from_object(vm, source.clone())?; + + // Check if buffer is writable + if buffer.desc.readonly { + return Err(vm.new_type_error("underlying buffer is not writable".to_owned())); + } + + // Get _type_ attribute directly + let type_attr = cls + .as_object() + .get_attr("_type_", vm) + .map_err(|_| vm.new_type_error(format!("'{}' has no _type_ attribute", cls.name())))?; + let type_str = type_attr.str(vm)?.to_string(); + let size = get_size(&type_str); + + // Check if buffer is large enough + let buffer_len = buffer.desc.len; + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Read bytes from buffer at offset + let bytes = buffer.obj_bytes(); + let data = &bytes[offset..offset + size]; + let value = bytes_to_pyobject(&cls, data, vm)?; + + // Create instance + let instance = PyCSimple::py_new(cls.clone(), (OptionalArg::Present(value),), vm)?; + + // TODO: Store reference to source in _objects to keep buffer alive + Ok(instance) + } + + #[pyclassmethod] + fn from_buffer_copy( + cls: PyTypeRef, + source: ArgBytesLike, + offset: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use super::_ctypes::get_size; + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + // Get _type_ attribute directly for simple types + let type_attr = cls + .as_object() + .get_attr("_type_", vm) + .map_err(|_| vm.new_type_error(format!("'{}' has no _type_ attribute", cls.name())))?; + let type_str = type_attr.str(vm)?.to_string(); + let size = get_size(&type_str); + + // Borrow bytes from source + let source_bytes = source.borrow_buf(); + let buffer_len = source_bytes.len(); + + // Check if buffer is large enough + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Copy bytes from buffer at offset + let data = &source_bytes[offset..offset + size]; + let value = bytes_to_pyobject(&cls, data, vm)?; + + // Create instance (independent copy, no reference tracking) + PyCSimple::py_new(cls.clone(), (OptionalArg::Present(value),), vm) + } + + #[pyclassmethod] + fn in_dll(cls: PyTypeRef, dll: PyObjectRef, name: PyStrRef, vm: &VirtualMachine) -> PyResult { + use super::_ctypes::get_size; + use libloading::Symbol; + + // Get the library handle from dll object + let handle = if let Ok(int_handle) = dll.try_int(vm) { + // dll is an integer handle + int_handle + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + } else { + // dll is a CDLL/PyDLL/WinDLL object with _handle attribute + dll.get_attr("_handle", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + }; + + // Get the library from cache + let library_cache = crate::stdlib::ctypes::library::libcache().read(); + let library = library_cache + .get_lib(handle) + .ok_or_else(|| vm.new_attribute_error("Library not found".to_owned()))?; + + // Get symbol address from library + let symbol_name = format!("{}\0", name.as_str()); + let inner_lib = library.lib.lock(); + + let symbol_address = if let Some(lib) = &*inner_lib { + unsafe { + // Try to get the symbol from the library + let symbol: Symbol<'_, *mut u8> = lib.get(symbol_name.as_bytes()).map_err(|e| { + vm.new_attribute_error(format!("{}: symbol '{}' not found", e, name.as_str())) + })?; + *symbol as usize + } + } else { + return Err(vm.new_attribute_error("Library is closed".to_owned())); + }; + + // Get _type_ attribute and size + let type_attr = cls + .as_object() + .get_attr("_type_", vm) + .map_err(|_| vm.new_type_error(format!("'{}' has no _type_ attribute", cls.name())))?; + let type_str = type_attr.str(vm)?.to_string(); + let size = get_size(&type_str); + + // Read value from symbol address + let value = if symbol_address != 0 && size > 0 { + // Safety: Reading from a symbol address provided by dlsym + unsafe { + let ptr = symbol_address as *const u8; + let bytes = std::slice::from_raw_parts(ptr, size); + bytes_to_pyobject(&cls, bytes, vm)? + } + } else { + vm.ctx.none() + }; + + // Create instance + let instance = PyCSimple::py_new(cls.clone(), (OptionalArg::Present(value),), vm)?; + + // Store base reference to keep dll alive + if let Ok(simple_ref) = instance.clone().downcast::() { + simple_ref.cdata.write().base = Some(dll); + } + + Ok(instance) + } } impl PyCSimple { @@ -372,3 +1008,40 @@ impl PyCSimple { None } } + +static SIMPLE_BUFFER_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| { + rustpython_common::lock::PyMappedRwLockReadGuard::map( + rustpython_common::lock::PyRwLockReadGuard::map( + buffer.obj_as::().cdata.read(), + |x: &CDataObject| x, + ), + |x: &CDataObject| x.buffer.as_slice(), + ) + .into() + }, + obj_bytes_mut: |buffer| { + rustpython_common::lock::PyMappedRwLockWriteGuard::map( + rustpython_common::lock::PyRwLockWriteGuard::map( + buffer.obj_as::().cdata.write(), + |x: &mut CDataObject| x, + ), + |x: &mut CDataObject| x.buffer.as_mut_slice(), + ) + .into() + }, + release: |_| {}, + retain: |_| {}, +}; + +impl AsBuffer for PyCSimple { + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let buffer_len = zelf.cdata.read().buffer.len(); + let buf = PyBuffer::new( + zelf.to_owned().into(), + BufferDescriptor::simple(buffer_len, false), // readonly=false for ctypes + &SIMPLE_BUFFER_METHODS, + ); + Ok(buf) + } +} diff --git a/crates/vm/src/stdlib/ctypes/field.rs b/crates/vm/src/stdlib/ctypes/field.rs index 05e4bdd035..8d6da5808a 100644 --- a/crates/vm/src/stdlib/ctypes/field.rs +++ b/crates/vm/src/stdlib/ctypes/field.rs @@ -5,6 +5,7 @@ use crate::{AsObject, Py, PyObjectRef, PyResult, VirtualMachine}; use num_traits::ToPrimitive; use super::structure::PyCStructure; +use super::union::PyCUnion; #[pyclass(name = "PyCFieldType", base = PyType, module = "_ctypes")] #[derive(PyPayload, Debug)] @@ -102,14 +103,23 @@ impl GetDescriptor for PyCField { _ => return Ok(zelf.into()), }; - // Instance attribute access - read value from the structure's buffer + // Instance attribute access - read value from the structure/union's buffer if let Some(structure) = obj.downcast_ref::() { - let buffer = structure.buffer.read(); + let cdata = structure.cdata.read(); let offset = zelf.byte_offset; let size = zelf.byte_size; - if offset + size <= buffer.len() { - let bytes = &buffer[offset..offset + size]; + if offset + size <= cdata.buffer.len() { + let bytes = &cdata.buffer[offset..offset + size]; + return PyCField::bytes_to_value(bytes, size, vm); + } + } else if let Some(union) = obj.downcast_ref::() { + let cdata = union.cdata.read(); + let offset = zelf.byte_offset; + let size = zelf.byte_size; + + if offset + size <= cdata.buffer.len() { + let bytes = &cdata.buffer[offset..offset + size]; return PyCField::bytes_to_value(bytes, size, vm); } } @@ -187,7 +197,7 @@ impl PyCField { .downcast_ref::() .ok_or_else(|| vm.new_type_error("expected CField".to_owned()))?; - // Get the structure instance - use downcast_ref() to access the struct data + // Get the structure/union instance - use downcast_ref() to access the struct data if let Some(structure) = obj.downcast_ref::() { match value { PySetterValue::Assign(value) => { @@ -195,9 +205,9 @@ impl PyCField { let size = zelf.byte_size; let bytes = PyCField::value_to_bytes(&value, size, vm)?; - let mut buffer = structure.buffer.write(); - if offset + size <= buffer.len() { - buffer[offset..offset + size].copy_from_slice(&bytes); + let mut cdata = structure.cdata.write(); + if offset + size <= cdata.buffer.len() { + cdata.buffer[offset..offset + size].copy_from_slice(&bytes); } Ok(()) } @@ -205,9 +215,26 @@ impl PyCField { Err(vm.new_type_error("cannot delete structure field".to_owned())) } } + } else if let Some(union) = obj.downcast_ref::() { + match value { + PySetterValue::Assign(value) => { + let offset = zelf.byte_offset; + let size = zelf.byte_size; + let bytes = PyCField::value_to_bytes(&value, size, vm)?; + + let mut cdata = union.cdata.write(); + if offset + size <= cdata.buffer.len() { + cdata.buffer[offset..offset + size].copy_from_slice(&bytes); + } + Ok(()) + } + PySetterValue::Delete => { + Err(vm.new_type_error("cannot delete union field".to_owned())) + } + } } else { Err(vm.new_type_error(format!( - "descriptor works only on Structure instances, got {}", + "descriptor works only on Structure or Union instances, got {}", obj.class().name() ))) } diff --git a/crates/vm/src/stdlib/ctypes/function.rs b/crates/vm/src/stdlib/ctypes/function.rs index c1db4d58f8..27e85c563e 100644 --- a/crates/vm/src/stdlib/ctypes/function.rs +++ b/crates/vm/src/stdlib/ctypes/function.rs @@ -5,9 +5,10 @@ use crate::convert::ToPyObject; use crate::function::FuncArgs; use crate::stdlib::ctypes::PyCData; use crate::stdlib::ctypes::base::{PyCSimple, ffi_type_from_str}; +use crate::stdlib::ctypes::thunk::PyCThunk; use crate::types::Representable; use crate::types::{Callable, Constructor}; -use crate::{AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine}; +use crate::{AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine}; use crossbeam_utils::atomic::AtomicCell; use libffi::middle::{Arg, Cif, CodePtr, Type}; use libloading::Symbol; @@ -76,10 +77,73 @@ impl ReturnType for PyTypeRef { fn from_ffi_type( &self, - _value: *mut ffi::c_void, - _vm: &VirtualMachine, + value: *mut ffi::c_void, + vm: &VirtualMachine, ) -> PyResult> { - todo!() + // Get the type code from _type_ attribute + let type_code = self + .get_class_attr(vm.ctx.intern_str("_type_")) + .and_then(|t| t.downcast_ref::().map(|s| s.to_string())); + + let result = match type_code.as_deref() { + Some("b") => vm + .ctx + .new_int(unsafe { *(value as *const i8) } as i32) + .into(), + Some("B") => vm + .ctx + .new_int(unsafe { *(value as *const u8) } as i32) + .into(), + Some("c") => vm + .ctx + .new_bytes(vec![unsafe { *(value as *const u8) }]) + .into(), + Some("h") => vm + .ctx + .new_int(unsafe { *(value as *const i16) } as i32) + .into(), + Some("H") => vm + .ctx + .new_int(unsafe { *(value as *const u16) } as i32) + .into(), + Some("i") => vm.ctx.new_int(unsafe { *(value as *const i32) }).into(), + Some("I") => vm.ctx.new_int(unsafe { *(value as *const u32) }).into(), + Some("l") => vm + .ctx + .new_int(unsafe { *(value as *const libc::c_long) }) + .into(), + Some("L") => vm + .ctx + .new_int(unsafe { *(value as *const libc::c_ulong) }) + .into(), + Some("q") => vm + .ctx + .new_int(unsafe { *(value as *const libc::c_longlong) }) + .into(), + Some("Q") => vm + .ctx + .new_int(unsafe { *(value as *const libc::c_ulonglong) }) + .into(), + Some("f") => vm + .ctx + .new_float(unsafe { *(value as *const f32) } as f64) + .into(), + Some("d") => vm.ctx.new_float(unsafe { *(value as *const f64) }).into(), + Some("P") | Some("z") | Some("Z") => vm.ctx.new_int(value as usize).into(), + Some("?") => vm + .ctx + .new_bool(unsafe { *(value as *const u8) } != 0) + .into(), + None => { + // No _type_ attribute, try to create an instance of the type + // This handles cases like Structure or Array return types + return Ok(Some( + vm.ctx.new_int(unsafe { *(value as *const i32) }).into(), + )); + } + _ => return Err(vm.new_type_error("Unsupported return type".to_string())), + }; + Ok(Some(result)) } } @@ -219,6 +283,49 @@ impl Constructor for PyCFuncPtr { .map(Into::into); } + // Check if first argument is a Python callable (callback creation) + if first_arg.is_callable() { + // Get argument types and result type from the class + let argtypes = cls.get_attr(vm.ctx.intern_str("_argtypes_")); + let restype = cls.get_attr(vm.ctx.intern_str("_restype_")); + + // Create the thunk (C-callable wrapper for the Python function) + let thunk = PyCThunk::new(first_arg.clone(), argtypes.clone(), restype.clone(), vm)?; + let code_ptr = thunk.code_ptr(); + + // Parse argument types for storage + let arg_type_vec: Option> = if let Some(ref args) = argtypes { + if vm.is_none(args) { + None + } else { + let mut types = Vec::new(); + for item in args.try_to_value::>(vm)? { + types.push(item.downcast::().map_err(|_| { + vm.new_type_error("_argtypes_ must be a sequence of types".to_string()) + })?); + } + Some(types) + } + } else { + None + }; + + // Store the thunk as a Python object to keep it alive + let thunk_ref: PyRef = thunk.into_ref(&vm.ctx); + + return Self { + ptr: PyRwLock::new(Some(code_ptr)), + needs_free: AtomicCell::new(true), + arg_types: PyRwLock::new(arg_type_vec), + _flags_: AtomicCell::new(0), + res_type: PyRwLock::new(restype), + name: PyRwLock::new(Some("".to_string())), + handler: thunk_ref.into(), + } + .into_ref_with_type(vm, cls) + .map(Into::into); + } + Err(vm.new_type_error("Expected an integer address or a tuple")) } } @@ -246,7 +353,8 @@ impl Callable for PyCFuncPtr { let return_type = zelf.res_type.read(); let ffi_return_type = return_type .as_ref() - .and_then(|t| ReturnType::to_ffi_type(&t.clone().downcast::().unwrap())) + .and_then(|t| t.clone().downcast::().ok()) + .and_then(|t| ReturnType::to_ffi_type(&t)) .unwrap_or_else(Type::i32); let cif = Cif::new(ffi_arg_types, ffi_return_type); @@ -269,14 +377,8 @@ impl Callable for PyCFuncPtr { let mut output: c_void = unsafe { cif.call(*code_ptr, &ffi_args) }; let return_type = return_type .as_ref() - .map(|f| { - f.clone() - .downcast::() - .unwrap() - .from_ffi_type(&mut output, vm) - .ok() - .flatten() - }) + .and_then(|f| f.clone().downcast::().ok()) + .map(|f| f.from_ffi_type(&mut output, vm).ok().flatten()) .unwrap_or_else(|| Some(vm.ctx.new_int(output as i32).as_object().to_pyobject(vm))); if let Some(return_type) = return_type { Ok(return_type) diff --git a/crates/vm/src/stdlib/ctypes/pointer.rs b/crates/vm/src/stdlib/ctypes/pointer.rs index 16d795ea4d..cd9ba8a7a1 100644 --- a/crates/vm/src/stdlib/ctypes/pointer.rs +++ b/crates/vm/src/stdlib/ctypes/pointer.rs @@ -6,33 +6,36 @@ use crate::builtins::{PyType, PyTypeRef}; use crate::convert::ToPyObject; use crate::protocol::PyNumberMethods; use crate::stdlib::ctypes::PyCData; -use crate::types::AsNumber; -use crate::{PyObjectRef, PyResult, VirtualMachine}; +use crate::types::{AsNumber, Constructor}; +use crate::{AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine}; + +use super::util::StgInfo; #[pyclass(name = "PyCPointerType", base = PyType, module = "_ctypes")] #[derive(PyPayload, Debug)] pub struct PyCPointerType { #[allow(dead_code)] - pub(crate) inner: PyCPointer, + pub stg_info: StgInfo, } #[pyclass(flags(IMMUTABLETYPE), with(AsNumber))] impl PyCPointerType { #[pymethod] fn __mul__(cls: PyTypeRef, n: isize, vm: &VirtualMachine) -> PyResult { - use super::array::{PyCArray, PyCArrayType}; + use super::array::PyCArrayType; if n < 0 { return Err(vm.new_value_error(format!("Array length must be >= 0, not {n}"))); } // Pointer size let element_size = std::mem::size_of::(); + let total_size = element_size * (n as usize); + let mut stg_info = super::util::StgInfo::new(total_size, element_size); + stg_info.length = n as usize; Ok(PyCArrayType { - inner: PyCArray { - typ: PyRwLock::new(cls), - length: AtomicCell::new(n as usize), - element_size: AtomicCell::new(element_size), - buffer: PyRwLock::new(vec![]), - }, + stg_info, + typ: PyRwLock::new(cls.as_object().to_owned()), + length: AtomicCell::new(n as usize), + element_size: AtomicCell::new(element_size), } .to_pyobject(vm)) } @@ -69,7 +72,23 @@ pub struct PyCPointer { contents: PyRwLock, } -#[pyclass(flags(BASETYPE, IMMUTABLETYPE))] +impl Constructor for PyCPointer { + type Args = (crate::function::OptionalArg,); + + fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult { + // Get the initial contents value if provided + let initial_contents = args.0.into_option().unwrap_or_else(|| vm.ctx.none()); + + // Create a new PyCPointer instance with the provided value + PyCPointer { + contents: PyRwLock::new(initial_contents), + } + .into_ref_with_type(vm, cls) + .map(Into::into) + } +} + +#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))] impl PyCPointer { // TODO: not correct #[pygetset] @@ -78,8 +97,173 @@ impl PyCPointer { Ok(contents) } #[pygetset(setter)] - fn set_contents(&self, contents: PyObjectRef) -> PyResult<()> { + fn set_contents(&self, contents: PyObjectRef, _vm: &VirtualMachine) -> PyResult<()> { + // Validate that the contents is a CData instance if we have a _type_ + // For now, just store it *self.contents.write() = contents; Ok(()) } + + #[pymethod] + fn __init__( + &self, + value: crate::function::OptionalArg, + _vm: &VirtualMachine, + ) -> PyResult<()> { + // Pointer can be initialized with 0 or 1 argument + // If 1 argument is provided, it should be a CData instance + if let crate::function::OptionalArg::Present(val) = value { + *self.contents.write() = val; + } + + Ok(()) + } + + #[pyclassmethod] + fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult { + if address == 0 { + return Err(vm.new_value_error("NULL pointer access".to_owned())); + } + // Pointer just stores the address value + Ok(PyCPointer { + contents: PyRwLock::new(vm.ctx.new_int(address).into()), + } + .into_ref_with_type(vm, cls)? + .into()) + } + + #[pyclassmethod] + fn from_buffer( + cls: PyTypeRef, + source: PyObjectRef, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use crate::TryFromObject; + use crate::protocol::PyBuffer; + + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + let size = std::mem::size_of::(); + + let buffer = PyBuffer::try_from_object(vm, source.clone())?; + + if buffer.desc.readonly { + return Err(vm.new_type_error("underlying buffer is not writable".to_owned())); + } + + let buffer_len = buffer.desc.len; + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Read pointer value from buffer + let bytes = buffer.obj_bytes(); + let ptr_bytes = &bytes[offset..offset + size]; + let ptr_val = usize::from_ne_bytes(ptr_bytes.try_into().expect("size is checked above")); + + Ok(PyCPointer { + contents: PyRwLock::new(vm.ctx.new_int(ptr_val).into()), + } + .into_ref_with_type(vm, cls)? + .into()) + } + + #[pyclassmethod] + fn from_buffer_copy( + cls: PyTypeRef, + source: crate::function::ArgBytesLike, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + let size = std::mem::size_of::(); + + let source_bytes = source.borrow_buf(); + let buffer_len = source_bytes.len(); + + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Read pointer value from buffer + let ptr_bytes = &source_bytes[offset..offset + size]; + let ptr_val = usize::from_ne_bytes(ptr_bytes.try_into().expect("size is checked above")); + + Ok(PyCPointer { + contents: PyRwLock::new(vm.ctx.new_int(ptr_val).into()), + } + .into_ref_with_type(vm, cls)? + .into()) + } + + #[pyclassmethod] + fn in_dll( + cls: PyTypeRef, + dll: PyObjectRef, + name: crate::builtins::PyStrRef, + vm: &VirtualMachine, + ) -> PyResult { + use libloading::Symbol; + + // Get the library handle from dll object + let handle = if let Ok(int_handle) = dll.try_int(vm) { + // dll is an integer handle + int_handle + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + } else { + // dll is a CDLL/PyDLL/WinDLL object with _handle attribute + dll.get_attr("_handle", vm)? + .try_int(vm)? + .as_bigint() + .to_usize() + .ok_or_else(|| vm.new_value_error("Invalid library handle".to_owned()))? + }; + + // Get the library from cache + let library_cache = crate::stdlib::ctypes::library::libcache().read(); + let library = library_cache + .get_lib(handle) + .ok_or_else(|| vm.new_attribute_error("Library not found".to_owned()))?; + + // Get symbol address from library + let symbol_name = format!("{}\0", name.as_str()); + let inner_lib = library.lib.lock(); + + let symbol_address = if let Some(lib) = &*inner_lib { + unsafe { + // Try to get the symbol from the library + let symbol: Symbol<'_, *mut u8> = lib.get(symbol_name.as_bytes()).map_err(|e| { + vm.new_attribute_error(format!("{}: symbol '{}' not found", e, name.as_str())) + })?; + *symbol as usize + } + } else { + return Err(vm.new_attribute_error("Library is closed".to_owned())); + }; + + // For pointer types, we return a pointer to the symbol address + Ok(PyCPointer { + contents: PyRwLock::new(vm.ctx.new_int(symbol_address).into()), + } + .into_ref_with_type(vm, cls)? + .into()) + } } diff --git a/crates/vm/src/stdlib/ctypes/structure.rs b/crates/vm/src/stdlib/ctypes/structure.rs index e7180a48e7..1c842e21b5 100644 --- a/crates/vm/src/stdlib/ctypes/structure.rs +++ b/crates/vm/src/stdlib/ctypes/structure.rs @@ -1,12 +1,13 @@ -use super::base::PyCData; +use super::base::{CDataObject, PyCData}; use super::field::PyCField; +use super::util::StgInfo; use crate::builtins::{PyList, PyStr, PyTuple, PyType, PyTypeRef}; use crate::convert::ToPyObject; use crate::function::FuncArgs; -use crate::protocol::PyNumberMethods; +use crate::protocol::{BufferDescriptor, BufferMethods, PyBuffer, PyNumberMethods}; use crate::stdlib::ctypes::_ctypes::get_size; -use crate::types::{AsNumber, Constructor}; -use crate::{AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine}; +use crate::types::{AsBuffer, AsNumber, Constructor}; +use crate::{AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine}; use crossbeam_utils::atomic::AtomicCell; use indexmap::IndexMap; use num_traits::ToPrimitive; @@ -16,7 +17,10 @@ use std::fmt::Debug; /// PyCStructType - metaclass for Structure #[pyclass(name = "PyCStructType", base = PyType, module = "_ctypes")] #[derive(Debug, PyPayload)] -pub struct PyCStructType {} +pub struct PyCStructType { + #[allow(dead_code)] + pub stg_info: StgInfo, +} impl Constructor for PyCStructType { type Args = FuncArgs; @@ -92,10 +96,10 @@ impl PyCStructType { let size = Self::get_field_size(&field_type, vm)?; // Create CField descriptor (accepts any ctypes type including arrays) - let cfield = PyCField::new(name.clone(), field_type, offset, size, index); + let c_field = PyCField::new(name.clone(), field_type, offset, size, index); // Set the CField as a class attribute - cls.set_attr(vm.ctx.intern_str(name), cfield.to_pyobject(vm)); + cls.set_attr(vm.ctx.intern_str(name), c_field.to_pyobject(vm)); offset += size; } @@ -133,22 +137,46 @@ impl PyCStructType { Ok(std::mem::size_of::()) } + /// Get the alignment of a ctypes type + fn get_field_align(field_type: &PyObjectRef, vm: &VirtualMachine) -> usize { + // Try to get _type_ attribute for simple types + if let Some(align) = field_type + .get_attr("_type_", vm) + .ok() + .and_then(|type_attr| type_attr.str(vm).ok()) + .and_then(|type_str| { + let s = type_str.to_string(); + (s.len() == 1).then(|| get_size(&s)) // alignment == size for simple types + }) + { + return align; + } + // Default alignment + 1 + } + #[pymethod] fn __mul__(cls: PyTypeRef, n: isize, vm: &VirtualMachine) -> PyResult { - use super::array::{PyCArray, PyCArrayType}; + use super::array::PyCArrayType; + use crate::stdlib::ctypes::_ctypes::size_of; + if n < 0 { return Err(vm.new_value_error(format!("Array length must be >= 0, not {n}"))); } - // TODO: Calculate element size properly - // For structures, element size is the structure size (sum of field sizes) - let element_size = std::mem::size_of::(); // Default, should calculate from fields + + // Calculate element size from the Structure type + let element_size = size_of(cls.clone().into(), vm)?; + + let total_size = element_size + .checked_mul(n as usize) + .ok_or_else(|| vm.new_overflow_error("array size too large".to_owned()))?; + let mut stg_info = super::util::StgInfo::new(total_size, element_size); + stg_info.length = n as usize; Ok(PyCArrayType { - inner: PyCArray { - typ: PyRwLock::new(cls), - length: AtomicCell::new(n as usize), - element_size: AtomicCell::new(element_size), - buffer: PyRwLock::new(vec![]), - }, + stg_info, + typ: PyRwLock::new(cls.clone().into()), + length: AtomicCell::new(n as usize), + element_size: AtomicCell::new(element_size), } .to_pyobject(vm)) } @@ -193,19 +221,17 @@ pub struct FieldInfo { )] #[derive(PyPayload)] pub struct PyCStructure { - /// Raw memory buffer for the structure - pub(super) buffer: PyRwLock>, + /// Common CDataObject for memory buffer + pub(super) cdata: PyRwLock, /// Field information (name -> FieldInfo) #[allow(dead_code)] pub(super) fields: PyRwLock>, - /// Total size of the structure - pub(super) size: AtomicCell, } impl Debug for PyCStructure { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PyCStructure") - .field("size", &self.size.load()) + .field("size", &self.cdata.read().size()) .finish() } } @@ -219,6 +245,7 @@ impl Constructor for PyCStructure { let mut fields_map = IndexMap::new(); let mut total_size = 0usize; + let mut max_align = 1usize; if let Some(fields_attr) = fields_attr { let fields: Vec = if let Some(list) = fields_attr.downcast_ref::() @@ -244,6 +271,8 @@ impl Constructor for PyCStructure { let name = name.to_string(); let field_type = field_tuple.get(1).unwrap().clone(); let size = PyCStructType::get_field_size(&field_type, vm)?; + let field_align = PyCStructType::get_field_align(&field_type, vm); + max_align = max_align.max(field_align); let type_ref = field_type .downcast::() @@ -265,12 +294,11 @@ impl Constructor for PyCStructure { } // Initialize buffer with zeros - let buffer = vec![0u8; total_size]; - + let mut stg_info = StgInfo::new(total_size, max_align); + stg_info.length = fields_map.len(); let instance = PyCStructure { - buffer: PyRwLock::new(buffer), + cdata: PyRwLock::new(CDataObject::from_stg_info(&stg_info)), fields: PyRwLock::new(fields_map.clone()), - size: AtomicCell::new(total_size), }; // Handle keyword arguments for field initialization @@ -305,9 +333,170 @@ impl Constructor for PyCStructure { #[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))] impl PyCStructure { + #[pygetset] + fn _objects(&self) -> Option { + self.cdata.read().objects.clone() + } + #[pygetset] fn _fields_(&self, vm: &VirtualMachine) -> PyObjectRef { // Return the _fields_ from the class, not instance vm.ctx.none() } + + #[pyclassmethod] + fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Read data from address + if address == 0 || size == 0 { + return Err(vm.new_value_error("NULL pointer access".to_owned())); + } + let data = unsafe { + let ptr = address as *const u8; + std::slice::from_raw_parts(ptr, size).to_vec() + }; + + // Create instance + Ok(PyCStructure { + cdata: PyRwLock::new(CDataObject::from_bytes(data, None)), + fields: PyRwLock::new(IndexMap::new()), + } + .into_ref_with_type(vm, cls)? + .into()) + } + + #[pyclassmethod] + fn from_buffer( + cls: PyTypeRef, + source: PyObjectRef, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use crate::TryFromObject; + use crate::protocol::PyBuffer; + use crate::stdlib::ctypes::_ctypes::size_of; + + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + // Get buffer from source + let buffer = PyBuffer::try_from_object(vm, source.clone())?; + + // Check if buffer is writable + if buffer.desc.readonly { + return Err(vm.new_type_error("underlying buffer is not writable".to_owned())); + } + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Check if buffer is large enough + let buffer_len = buffer.desc.len; + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Read bytes from buffer at offset + let bytes = buffer.obj_bytes(); + let data = bytes[offset..offset + size].to_vec(); + + // Create instance + Ok(PyCStructure { + cdata: PyRwLock::new(CDataObject::from_bytes(data, Some(source))), + fields: PyRwLock::new(IndexMap::new()), + } + .into_ref_with_type(vm, cls)? + .into()) + } + + #[pyclassmethod] + fn from_buffer_copy( + cls: PyTypeRef, + source: crate::function::ArgBytesLike, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Borrow bytes from source + let source_bytes = source.borrow_buf(); + let buffer_len = source_bytes.len(); + + // Check if buffer is large enough + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Copy bytes from buffer at offset + let data = source_bytes[offset..offset + size].to_vec(); + + // Create instance + Ok(PyCStructure { + cdata: PyRwLock::new(CDataObject::from_bytes(data, None)), + fields: PyRwLock::new(IndexMap::new()), + } + .into_ref_with_type(vm, cls)? + .into()) + } +} + +static STRUCTURE_BUFFER_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| { + rustpython_common::lock::PyMappedRwLockReadGuard::map( + rustpython_common::lock::PyRwLockReadGuard::map( + buffer.obj_as::().cdata.read(), + |x: &CDataObject| x, + ), + |x: &CDataObject| x.buffer.as_slice(), + ) + .into() + }, + obj_bytes_mut: |buffer| { + rustpython_common::lock::PyMappedRwLockWriteGuard::map( + rustpython_common::lock::PyRwLockWriteGuard::map( + buffer.obj_as::().cdata.write(), + |x: &mut CDataObject| x, + ), + |x: &mut CDataObject| x.buffer.as_mut_slice(), + ) + .into() + }, + release: |_| {}, + retain: |_| {}, +}; + +impl AsBuffer for PyCStructure { + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let buffer_len = zelf.cdata.read().buffer.len(); + let buf = PyBuffer::new( + zelf.to_owned().into(), + BufferDescriptor::simple(buffer_len, false), // readonly=false for ctypes + &STRUCTURE_BUFFER_METHODS, + ); + Ok(buf) + } } diff --git a/crates/vm/src/stdlib/ctypes/thunk.rs b/crates/vm/src/stdlib/ctypes/thunk.rs index a65b04684b..2de2308e1a 100644 --- a/crates/vm/src/stdlib/ctypes/thunk.rs +++ b/crates/vm/src/stdlib/ctypes/thunk.rs @@ -1,22 +1,319 @@ -//! Yes, really, this is not a typo. - -// typedef struct { -// PyObject_VAR_HEAD -// ffi_closure *pcl_write; /* the C callable, writeable */ -// void *pcl_exec; /* the C callable, executable */ -// ffi_cif cif; -// int flags; -// PyObject *converters; -// PyObject *callable; -// PyObject *restype; -// SETFUNC setfunc; -// ffi_type *ffi_restype; -// ffi_type *atypes[1]; -// } CThunkObject; +//! FFI callback (thunk) implementation for ctypes. +//! +//! This module implements CThunkObject which wraps Python callables +//! to be callable from C code via libffi closures. +use crate::builtins::{PyStr, PyType, PyTypeRef}; +use crate::vm::thread::with_current_vm; +use crate::{PyObjectRef, PyPayload, PyResult, VirtualMachine}; +use libffi::low; +use libffi::middle::{Cif, Closure, CodePtr, Type}; +use num_traits::ToPrimitive; +use rustpython_common::lock::PyRwLock; +use std::ffi::c_void; +use std::fmt::Debug; + +use super::base::ffi_type_from_str; +/// Userdata passed to the libffi callback. +/// This contains everything needed to invoke the Python callable. +pub struct ThunkUserData { + /// The Python callable to invoke + pub callable: PyObjectRef, + /// Argument types for conversion + pub arg_types: Vec, + /// Result type for conversion (None means void) + pub res_type: Option, +} + +/// Get the type code string from a ctypes type +fn get_type_code(ty: &PyTypeRef, vm: &VirtualMachine) -> Option { + ty.get_attr(vm.ctx.intern_str("_type_")) + .and_then(|t| t.downcast_ref::().map(|s| s.to_string())) +} + +/// Convert a C value to a Python object based on the type code +fn ffi_to_python(ty: &PyTypeRef, ptr: *const c_void, vm: &VirtualMachine) -> PyObjectRef { + let type_code = get_type_code(ty, vm); + // SAFETY: ptr is guaranteed to be valid by libffi calling convention + unsafe { + match type_code.as_deref() { + Some("b") => vm.ctx.new_int(*(ptr as *const i8) as i32).into(), + Some("B") => vm.ctx.new_int(*(ptr as *const u8) as i32).into(), + Some("c") => vm.ctx.new_bytes(vec![*(ptr as *const u8)]).into(), + Some("h") => vm.ctx.new_int(*(ptr as *const i16) as i32).into(), + Some("H") => vm.ctx.new_int(*(ptr as *const u16) as i32).into(), + Some("i") => vm.ctx.new_int(*(ptr as *const i32)).into(), + Some("I") => vm.ctx.new_int(*(ptr as *const u32)).into(), + Some("l") => vm.ctx.new_int(*(ptr as *const libc::c_long)).into(), + Some("L") => vm.ctx.new_int(*(ptr as *const libc::c_ulong)).into(), + Some("q") => vm.ctx.new_int(*(ptr as *const libc::c_longlong)).into(), + Some("Q") => vm.ctx.new_int(*(ptr as *const libc::c_ulonglong)).into(), + Some("f") => vm.ctx.new_float(*(ptr as *const f32) as f64).into(), + Some("d") => vm.ctx.new_float(*(ptr as *const f64)).into(), + Some("P") | Some("z") | Some("Z") => vm.ctx.new_int(ptr as usize).into(), + _ => vm.ctx.none(), + } + } +} + +/// Convert a Python object to a C value and store it at the result pointer +fn python_to_ffi(obj: PyResult, ty: &PyTypeRef, result: *mut c_void, vm: &VirtualMachine) { + let obj = match obj { + Ok(o) => o, + Err(_) => return, // Exception occurred, leave result as-is + }; + + let type_code = get_type_code(ty, vm); + // SAFETY: result is guaranteed to be valid by libffi calling convention + unsafe { + match type_code.as_deref() { + Some("b") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut i8) = i.as_bigint().to_i8().unwrap_or(0); + } + } + Some("B") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut u8) = i.as_bigint().to_u8().unwrap_or(0); + } + } + Some("c") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut u8) = i.as_bigint().to_u8().unwrap_or(0); + } + } + Some("h") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut i16) = i.as_bigint().to_i16().unwrap_or(0); + } + } + Some("H") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut u16) = i.as_bigint().to_u16().unwrap_or(0); + } + } + Some("i") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut i32) = i.as_bigint().to_i32().unwrap_or(0); + } + } + Some("I") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut u32) = i.as_bigint().to_u32().unwrap_or(0); + } + } + Some("l") | Some("q") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut i64) = i.as_bigint().to_i64().unwrap_or(0); + } + } + Some("L") | Some("Q") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut u64) = i.as_bigint().to_u64().unwrap_or(0); + } + } + Some("f") => { + if let Ok(f) = obj.try_float(vm) { + *(result as *mut f32) = f.to_f64() as f32; + } + } + Some("d") => { + if let Ok(f) = obj.try_float(vm) { + *(result as *mut f64) = f.to_f64(); + } + } + Some("P") | Some("z") | Some("Z") => { + if let Ok(i) = obj.try_int(vm) { + *(result as *mut usize) = i.as_bigint().to_usize().unwrap_or(0); + } + } + _ => {} + } + } +} + +/// The callback function that libffi calls when the closure is invoked. +/// This function converts C arguments to Python objects, calls the Python +/// callable, and converts the result back to C. +unsafe extern "C" fn thunk_callback( + _cif: &low::ffi_cif, + result: &mut c_void, + args: *const *const c_void, + userdata: &ThunkUserData, +) { + with_current_vm(|vm| { + // Convert C arguments to Python objects + let py_args: Vec = userdata + .arg_types + .iter() + .enumerate() + .map(|(i, ty)| { + let arg_ptr = unsafe { *args.add(i) }; + ffi_to_python(ty, arg_ptr, vm) + }) + .collect(); + + // Call the Python callable + let py_result = userdata.callable.call(py_args, vm); + + // Convert result back to C type + if let Some(ref res_type) = userdata.res_type { + python_to_ffi(py_result, res_type, result as *mut c_void, vm); + } + }); +} + +/// Holds the closure and userdata together to ensure proper lifetime. +/// The userdata is leaked to create a 'static reference that the closure can use. +struct ThunkData { + #[allow(dead_code)] + closure: Closure<'static>, + /// Raw pointer to the leaked userdata, for cleanup + userdata_ptr: *mut ThunkUserData, +} + +impl Drop for ThunkData { + fn drop(&mut self) { + // SAFETY: We created this with Box::into_raw, so we can reclaim it + unsafe { + drop(Box::from_raw(self.userdata_ptr)); + } + } +} + +/// CThunkObject wraps a Python callable to make it callable from C code. #[pyclass(name = "CThunkObject", module = "_ctypes")] -#[derive(Debug, PyPayload)] -pub struct PyCThunk {} +#[derive(PyPayload)] +pub struct PyCThunk { + /// The Python callable + callable: PyObjectRef, + /// The libffi closure (must be kept alive) + #[allow(dead_code)] + thunk_data: PyRwLock>, + /// The code pointer for the closure + code_ptr: CodePtr, +} + +impl Debug for PyCThunk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PyCThunk") + .field("callable", &self.callable) + .finish() + } +} + +impl PyCThunk { + /// Create a new thunk wrapping a Python callable. + /// + /// # Arguments + /// * `callable` - The Python callable to wrap + /// * `arg_types` - Optional sequence of argument types + /// * `res_type` - Optional result type + /// * `vm` - The virtual machine + pub fn new( + callable: PyObjectRef, + arg_types: Option, + res_type: Option, + vm: &VirtualMachine, + ) -> PyResult { + // Parse argument types + let arg_type_vec: Vec = if let Some(args) = arg_types { + if vm.is_none(&args) { + Vec::new() + } else { + let mut types = Vec::new(); + for item in args.try_to_value::>(vm)? { + types.push(item.downcast::().map_err(|_| { + vm.new_type_error("_argtypes_ must be a sequence of types".to_string()) + })?); + } + types + } + } else { + Vec::new() + }; + + // Parse result type + let res_type_ref: Option = + if let Some(ref rt) = res_type { + if vm.is_none(rt) { + None + } else { + Some(rt.clone().downcast::().map_err(|_| { + vm.new_type_error("restype must be a ctypes type".to_string()) + })?) + } + } else { + None + }; + + // Build FFI types + let ffi_arg_types: Vec = arg_type_vec + .iter() + .map(|ty| { + get_type_code(ty, vm) + .and_then(|code| ffi_type_from_str(&code)) + .unwrap_or(Type::pointer()) + }) + .collect(); + + let ffi_res_type = res_type_ref + .as_ref() + .and_then(|ty| get_type_code(ty, vm)) + .and_then(|code| ffi_type_from_str(&code)) + .unwrap_or(Type::void()); + + // Create the CIF + let cif = Cif::new(ffi_arg_types, ffi_res_type); + + // Create userdata and leak it to get a 'static reference + let userdata = Box::new(ThunkUserData { + callable: callable.clone(), + arg_types: arg_type_vec, + res_type: res_type_ref, + }); + let userdata_ptr = Box::into_raw(userdata); + + // SAFETY: We maintain the userdata lifetime by storing it in ThunkData + // and cleaning it up in Drop + let userdata_ref: &'static ThunkUserData = unsafe { &*userdata_ptr }; + + // Create the closure + let closure = Closure::new(cif, thunk_callback, userdata_ref); + + // Get the code pointer + let code_ptr = CodePtr(*closure.code_ptr() as *mut _); + + // Store closure and userdata together + let thunk_data = ThunkData { + closure, + userdata_ptr, + }; + + Ok(Self { + callable, + thunk_data: PyRwLock::new(Some(thunk_data)), + code_ptr, + }) + } + + /// Get the code pointer for this thunk + pub fn code_ptr(&self) -> CodePtr { + self.code_ptr + } +} + +// SAFETY: PyCThunk is safe to send/sync because: +// - callable is a PyObjectRef which is Send+Sync +// - thunk_data contains the libffi closure which is heap-allocated +// - code_ptr is just a pointer to executable memory +unsafe impl Send for PyCThunk {} +unsafe impl Sync for PyCThunk {} #[pyclass] -impl PyCThunk {} +impl PyCThunk { + #[pygetset] + fn callable(&self) -> PyObjectRef { + self.callable.clone() + } +} diff --git a/crates/vm/src/stdlib/ctypes/union.rs b/crates/vm/src/stdlib/ctypes/union.rs index a357c195d6..aa78d56c46 100644 --- a/crates/vm/src/stdlib/ctypes/union.rs +++ b/crates/vm/src/stdlib/ctypes/union.rs @@ -1,17 +1,30 @@ -use super::base::PyCData; +use super::base::{CDataObject, PyCData}; use super::field::PyCField; +use super::util::StgInfo; use crate::builtins::{PyList, PyStr, PyTuple, PyType, PyTypeRef}; use crate::convert::ToPyObject; use crate::function::FuncArgs; +use crate::protocol::{BufferDescriptor, BufferMethods, PyBuffer as ProtocolPyBuffer}; use crate::stdlib::ctypes::_ctypes::get_size; -use crate::types::Constructor; -use crate::{PyObjectRef, PyPayload, PyResult, VirtualMachine}; +use crate::types::{AsBuffer, Constructor}; +use crate::{AsObject, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine}; use num_traits::ToPrimitive; +use rustpython_common::lock::PyRwLock; /// PyCUnionType - metaclass for Union #[pyclass(name = "UnionType", base = PyType, module = "_ctypes")] #[derive(Debug, PyPayload)] -pub struct PyCUnionType {} +pub struct PyCUnionType { + pub stg_info: StgInfo, +} + +impl Default for PyCUnionType { + fn default() -> Self { + PyCUnionType { + stg_info: StgInfo::new(0, 1), + } + } +} impl Constructor for PyCUnionType { type Args = FuncArgs; @@ -74,9 +87,9 @@ impl PyCUnionType { // For Union, all fields start at offset 0 // Create CField descriptor (accepts any ctypes type including arrays) - let cfield = PyCField::new(name.clone(), field_type, 0, size, index); + let c_field = PyCField::new(name.clone(), field_type, 0, size, index); - cls.set_attr(vm.ctx.intern_str(name), cfield.to_pyobject(vm)); + cls.set_attr(vm.ctx.intern_str(name), c_field.to_pyobject(vm)); } Ok(()) @@ -114,7 +127,202 @@ impl PyCUnionType {} /// PyCUnion - base class for Union #[pyclass(module = "_ctypes", name = "Union", base = PyCData, metaclass = "PyCUnionType")] -pub struct PyCUnion {} +#[derive(PyPayload)] +pub struct PyCUnion { + /// Common CDataObject for memory buffer + pub(super) cdata: PyRwLock, +} + +impl std::fmt::Debug for PyCUnion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PyCUnion") + .field("size", &self.cdata.read().size()) + .finish() + } +} + +impl Constructor for PyCUnion { + type Args = FuncArgs; + + fn py_new(cls: PyTypeRef, _args: Self::Args, vm: &VirtualMachine) -> PyResult { + // Get _fields_ from the class + let fields_attr = cls.as_object().get_attr("_fields_", vm).ok(); + + // Calculate union size (max of all field sizes) and alignment + let mut max_size = 0usize; + let mut max_align = 1usize; + + if let Some(fields_attr) = fields_attr { + let fields: Vec = if let Some(list) = fields_attr.downcast_ref::() + { + list.borrow_vec().to_vec() + } else if let Some(tuple) = fields_attr.downcast_ref::() { + tuple.to_vec() + } else { + vec![] + }; + + for field in fields.iter() { + let Some(field_tuple) = field.downcast_ref::() else { + continue; + }; + if field_tuple.len() < 2 { + continue; + } + let field_type = field_tuple.get(1).unwrap().clone(); + let size = PyCUnionType::get_field_size(&field_type, vm)?; + max_size = max_size.max(size); + // For simple types, alignment == size + max_align = max_align.max(size); + } + } + + // Initialize buffer with zeros + let stg_info = StgInfo::new(max_size, max_align); + PyCUnion { + cdata: PyRwLock::new(CDataObject::from_stg_info(&stg_info)), + } + .into_ref_with_type(vm, cls) + .map(Into::into) + } +} + +#[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor, AsBuffer))] +impl PyCUnion { + #[pygetset] + fn _objects(&self) -> Option { + self.cdata.read().objects.clone() + } + + #[pyclassmethod] + fn from_address(cls: PyTypeRef, address: isize, vm: &VirtualMachine) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + + // Get size from cls + let size = size_of(cls.clone().into(), vm)?; + + // Create instance with data from address + if address == 0 || size == 0 { + return Err(vm.new_value_error("NULL pointer access".to_owned())); + } + let stg_info = StgInfo::new(size, 1); + Ok(PyCUnion { + cdata: PyRwLock::new(CDataObject::from_stg_info(&stg_info)), + } + .into_ref_with_type(vm, cls)? + .into()) + } -#[pyclass(flags(BASETYPE, IMMUTABLETYPE))] -impl PyCUnion {} + #[pyclassmethod] + fn from_buffer( + cls: PyTypeRef, + source: PyObjectRef, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use crate::TryFromObject; + use crate::protocol::PyBuffer; + use crate::stdlib::ctypes::_ctypes::size_of; + + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + let buffer = PyBuffer::try_from_object(vm, source.clone())?; + + if buffer.desc.readonly { + return Err(vm.new_type_error("underlying buffer is not writable".to_owned())); + } + + let size = size_of(cls.clone().into(), vm)?; + let buffer_len = buffer.desc.len; + + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Copy data from source buffer + let bytes = buffer.obj_bytes(); + let data = bytes[offset..offset + size].to_vec(); + + Ok(PyCUnion { + cdata: PyRwLock::new(CDataObject::from_bytes(data, None)), + } + .into_ref_with_type(vm, cls)? + .into()) + } + + #[pyclassmethod] + fn from_buffer_copy( + cls: PyTypeRef, + source: crate::function::ArgBytesLike, + offset: crate::function::OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + use crate::stdlib::ctypes::_ctypes::size_of; + + let offset = offset.unwrap_or(0); + if offset < 0 { + return Err(vm.new_value_error("offset cannot be negative".to_owned())); + } + let offset = offset as usize; + + let size = size_of(cls.clone().into(), vm)?; + let source_bytes = source.borrow_buf(); + let buffer_len = source_bytes.len(); + + if offset + size > buffer_len { + return Err(vm.new_value_error(format!( + "Buffer size too small ({} instead of at least {} bytes)", + buffer_len, + offset + size + ))); + } + + // Copy data from source + let data = source_bytes[offset..offset + size].to_vec(); + + Ok(PyCUnion { + cdata: PyRwLock::new(CDataObject::from_bytes(data, None)), + } + .into_ref_with_type(vm, cls)? + .into()) + } +} + +static UNION_BUFFER_METHODS: BufferMethods = BufferMethods { + obj_bytes: |buffer| { + rustpython_common::lock::PyRwLockReadGuard::map( + buffer.obj_as::().cdata.read(), + |x: &CDataObject| x.buffer.as_slice(), + ) + .into() + }, + obj_bytes_mut: |buffer| { + rustpython_common::lock::PyRwLockWriteGuard::map( + buffer.obj_as::().cdata.write(), + |x: &mut CDataObject| x.buffer.as_mut_slice(), + ) + .into() + }, + release: |_| {}, + retain: |_| {}, +}; + +impl AsBuffer for PyCUnion { + fn as_buffer(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let buffer_len = zelf.cdata.read().buffer.len(); + let buf = ProtocolPyBuffer::new( + zelf.to_owned().into(), + BufferDescriptor::simple(buffer_len, false), // readonly=false for ctypes + &UNION_BUFFER_METHODS, + ); + Ok(buf) + } +} diff --git a/crates/vm/src/stdlib/ctypes/util.rs b/crates/vm/src/stdlib/ctypes/util.rs new file mode 100644 index 0000000000..b8fd9c89c0 --- /dev/null +++ b/crates/vm/src/stdlib/ctypes/util.rs @@ -0,0 +1,41 @@ +use crate::PyObjectRef; + +/// Storage information for ctypes types +#[derive(Debug, Clone)] +pub struct StgInfo { + #[allow(dead_code)] + pub initialized: bool, + pub size: usize, // number of bytes + pub align: usize, // alignment requirements + pub length: usize, // number of fields (for arrays/structures) + #[allow(dead_code)] + pub proto: Option, // Only for Pointer/ArrayObject + #[allow(dead_code)] + pub flags: i32, // calling convention and such +} + +impl Default for StgInfo { + fn default() -> Self { + StgInfo { + initialized: false, + size: 0, + align: 1, + length: 0, + proto: None, + flags: 0, + } + } +} + +impl StgInfo { + pub fn new(size: usize, align: usize) -> Self { + StgInfo { + initialized: true, + size, + align, + length: 0, + proto: None, + flags: 0, + } + } +}