diff --git a/crates/capi/src/descrobject.rs b/crates/capi/src/descrobject.rs index 5c7001554..ea27600bb 100644 --- a/crates/capi/src/descrobject.rs +++ b/crates/capi/src/descrobject.rs @@ -130,7 +130,8 @@ impl PyMemberDef { let mut flags = self.flags; if flags & PY_RELATIVE_OFFSET != 0 { // type creation adds tp_basicsize and clears the flag before GetOne. - offset += (rustpython_vm::object::SIZEOF_PYOBJECT_HEAD + ty.slots.basicsize) as isize; + // `slots.basicsize` is already the full tp_basicsize. + offset += ty.slots.basicsize as isize; flags &= !PY_RELATIVE_OFFSET; } diff --git a/crates/capi/src/object/pytype.rs b/crates/capi/src/object/pytype.rs index 15e83634d..d0ae62b1b 100644 --- a/crates/capi/src/object/pytype.rs +++ b/crates/capi/src/object/pytype.rs @@ -59,7 +59,7 @@ pub unsafe extern "C" fn PyType_GetFullyQualifiedName(ptr: *mut PyTypeObject) -> with_vm(|vm| { let ty = unsafe { ptr.assume_borrowed() }; let qualname = ty.__qualname__(vm).try_downcast::(vm)?; - let module = ty.__module__(vm); + let module = ty.__module__(vm)?; if let Some(module) = module.downcast_ref::() && module.as_wtf8() != "builtins" diff --git a/crates/compiler-core/src/bytecode.rs b/crates/compiler-core/src/bytecode.rs index 641601d92..f7d117cb7 100644 --- a/crates/compiler-core/src/bytecode.rs +++ b/crates/compiler-core/src/bytecode.rs @@ -520,6 +520,11 @@ bitflags! { } } +const _: () = { + assert!(core::mem::size_of::() == core::mem::size_of::()); + assert!(core::mem::align_of::() == core::mem::align_of::()); +}; + impl CodeFlags { /// The `__future__` flags that `compile()` accepts and that a compiled code /// object inherits from its caller. Mirrors `PyCF_MASK`. diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index bf745f973..ccb6a7543 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -551,12 +551,14 @@ fn generate_class_def( } }); // If repr(transparent) with a base, the type has the same memory layout as base, - // so basicsize should be 0 (no additional space beyond the base type) - // Otherwise, basicsize = sizeof(payload). The header size is added in __basicsize__ getter. + // so basicsize stays 0 and type creation copies the base's full tp_basicsize. + // Otherwise, basicsize is the object header plus the payload. let basicsize = if is_repr_transparent && base.is_some() { quote!(0) } else { - quote!(std::mem::size_of::<#ident>()) + quote!( + ::rustpython_vm::object::SIZEOF_PYOBJECT_HEAD + ::core::mem::size_of::<#ident>() + ) }; if base.is_some() && is_pystruct { bail_span!(ident, "PyStructSequence cannot have `base` class attr",); @@ -914,8 +916,13 @@ pub(crate) fn impl_pyexception(attr: PunctuatedNestedMeta, item: &Item) -> Resul None => quote! {}, }; + let module_attr = match class_meta.optional_module()? { + Some(module) => quote! { module = #module, }, + None => quote! { module = false, }, + }; + let ret = quote! { - #[pyclass(module = false, name = #class_name, base = #base_class_name #traverse_attr #payload_attr)] + #[pyclass(#module_attr name = #class_name, base = #base_class_name #traverse_attr #payload_attr)] #item #impl_pyclass }; diff --git a/crates/derive-impl/src/util.rs b/crates/derive-impl/src/util.rs index 3b520166b..ef6f115cf 100644 --- a/crates/derive-impl/src/util.rs +++ b/crates/derive-impl/src/util.rs @@ -534,6 +534,15 @@ impl ExceptionItemMeta { pub(crate) fn has_impl(&self) -> Result { self.inner()._bool("impl") } + + /// `module = "..."` when present. Omitted means a builtin exception. + pub(crate) fn optional_module(&self) -> Result> { + if self.inner().meta_map.contains_key("module") { + self.module() + } else { + Ok(None) + } + } } impl core::ops::Deref for ExceptionItemMeta { diff --git a/crates/host_env/src/multiprocessing.rs b/crates/host_env/src/multiprocessing.rs index 26832cf24..965dc5677 100644 --- a/crates/host_env/src/multiprocessing.rs +++ b/crates/host_env/src/multiprocessing.rs @@ -18,6 +18,7 @@ use libc::sem_t; use nix::errno::Errno; #[cfg(unix)] +#[repr(transparent)] #[derive(Debug)] pub struct SemHandle { raw: *mut sem_t, @@ -118,6 +119,7 @@ pub type RawSocket = SOCKET; pub const INFINITE_TIMEOUT: u32 = INFINITE; #[cfg(windows)] +#[repr(transparent)] #[derive(Debug)] pub struct SemHandle { raw: HANDLE, diff --git a/crates/stdlib/src/bz2.rs b/crates/stdlib/src/bz2.rs index 353eddee3..f1590c4d4 100644 --- a/crates/stdlib/src/bz2.rs +++ b/crates/stdlib/src/bz2.rs @@ -6,14 +6,15 @@ pub(crate) use _bz2::module_def; mod _bz2 { use crate::compression::DecompressorArgs; use crate::vm::{ - Py, VirtualMachine, - builtins::{PyBaseExceptionRef, PyBytesRef, PyType}, + Py, PyObject, VirtualMachine, + builtins::{PyBaseExceptionRef, PyBytes, PyType}, common::lock::PyMutex, function::ArgBytesLike, - object::PyResult, + object::{PyAtomicRef, PyResult}, types::Constructor, }; use alloc::fmt; + use core::sync::atomic::{AtomicBool, Ordering}; use rustpython_common::compression::bz2 as backend; fn map_bz2_error(error: backend::Bz2Error, vm: &VirtualMachine) -> PyBaseExceptionRef { @@ -31,15 +32,6 @@ mod _bz2 { struct PyBZ2DecompressorInner { decompress: backend::Decompressor, - unused_data: PyBytesRef, - } - - impl PyBZ2DecompressorInner { - fn sync_visible_state(&mut self, vm: &VirtualMachine) { - if self.unused_data.as_bytes() != self.decompress.unused_data() { - self.unused_data = vm.ctx.new_bytes(self.decompress.unused_data().to_vec()); - } - } } #[pyattr] @@ -48,6 +40,14 @@ mod _bz2 { struct BZ2Decompressor { #[pytraverse(skip)] inner: PyMutex, + #[pymember] + #[pytraverse(skip)] + eof: AtomicBool, + #[pymember] + #[pytraverse(skip)] + needs_input: AtomicBool, + #[pymember(type = "object_ex")] + unused_data: PyAtomicRef>, } impl fmt::Debug for BZ2Decompressor { @@ -63,8 +63,10 @@ mod _bz2 { Ok(Self { inner: PyMutex::new(PyBZ2DecompressorInner { decompress: backend::Decompressor::new(), - unused_data: vm.ctx.empty_bytes.clone(), }), + eof: AtomicBool::new(false), + needs_input: AtomicBool::new(true), + unused_data: PyAtomicRef::from(Some(vm.ctx.empty_bytes.clone().into())), }) } } @@ -84,25 +86,21 @@ mod _bz2 { return Err(vm.new_value_error("Decompressor is unusable after a previous error")); } let result = inner.decompress.decompress(data, max_length); - inner.sync_visible_state(vm); + self.eof.store(inner.decompress.eof(), Ordering::Relaxed); + self.needs_input + .store(inner.decompress.needs_input(), Ordering::Relaxed); + let stale = self.unused_data.deref().is_none_or(|obj| { + obj.downcast_ref::() + .is_none_or(|bytes| bytes.as_bytes() != inner.decompress.unused_data()) + }); + if stale { + let bytes = vm.ctx.new_bytes(inner.decompress.unused_data().to_vec()); + // The previous bytes object is dropped after the slot is replaced. + let _previous = unsafe { self.unused_data.swap(Some(bytes.into())) }; + } result.map_err(|error| map_bz2_error(error, vm)) } - #[pygetset] - fn eof(&self) -> bool { - self.inner.lock().decompress.eof() - } - - #[pygetset] - fn unused_data(&self) -> PyBytesRef { - self.inner.lock().unused_data.clone() - } - - #[pygetset] - fn needs_input(&self) -> bool { - self.inner.lock().decompress.needs_input() - } - #[pymethod(name = "__reduce__")] fn reduce(&self, vm: &VirtualMachine) -> PyResult<()> { Err(vm.new_type_error("cannot pickle '_bz2.BZ2Decompressor' object")) diff --git a/crates/stdlib/src/cjkcodecs/multibytecodec.rs b/crates/stdlib/src/cjkcodecs/multibytecodec.rs index 32b67e3ed..84cdc1fcd 100644 --- a/crates/stdlib/src/cjkcodecs/multibytecodec.rs +++ b/crates/stdlib/src/cjkcodecs/multibytecodec.rs @@ -974,6 +974,7 @@ mod _multibytecodec { struct MultibyteStreamReader { #[pytraverse(skip)] codec: CodecRef, + #[pymember] stream: PyObjectRef, #[pytraverse(skip)] inner: PyMutex, @@ -1006,11 +1007,6 @@ mod _multibytecodec { #[pyclass(with(Constructor, Initializer), flags(BASETYPE))] impl MultibyteStreamReader { - #[pygetset] - fn stream(&self) -> PyObjectRef { - self.stream.clone() - } - #[pygetset] fn errors(&self, vm: &VirtualMachine) -> PyStrRef { self.inner.lock().errors.name(vm) @@ -1134,6 +1130,7 @@ mod _multibytecodec { struct MultibyteStreamWriter { #[pytraverse(skip)] codec: CodecRef, + #[pymember] stream: PyObjectRef, #[pytraverse(skip)] inner: PyMutex, @@ -1166,11 +1163,6 @@ mod _multibytecodec { #[pyclass(with(Constructor, Initializer), flags(BASETYPE))] impl MultibyteStreamWriter { - #[pygetset] - fn stream(&self) -> PyObjectRef { - self.stream.clone() - } - #[pygetset] fn errors(&self, vm: &VirtualMachine) -> PyStrRef { self.inner.lock().errors.name(vm) diff --git a/crates/stdlib/src/contextvars.rs b/crates/stdlib/src/contextvars.rs index 927f605fd..0c1adf9d7 100644 --- a/crates/stdlib/src/contextvars.rs +++ b/crates/stdlib/src/contextvars.rs @@ -359,6 +359,7 @@ mod _contextvars { #[pyclass(name, traverse)] #[derive(PyPayload)] struct ContextVar { + #[pymember] name: PyStrRef, default: Option, #[pytraverse(skip)] @@ -436,11 +437,6 @@ mod _contextvars { #[pyclass(with(Constructor, Hashable, Representable))] impl ContextVar { - #[pygetset] - fn name(&self) -> PyStrRef { - self.name.clone() - } - #[pymethod] fn get( zelf: &Py, diff --git a/crates/stdlib/src/elementtree.rs b/crates/stdlib/src/elementtree.rs index 92963af66..df2ee4d81 100644 --- a/crates/stdlib/src/elementtree.rs +++ b/crates/stdlib/src/elementtree.rs @@ -1829,11 +1829,9 @@ pub(crate) mod _elementtree { /// The `pyexpat.xmlparser` doing the actual scanning, or `None` /// before `__init__` (and after `close()` dropped it). parser: Option, - target: Option, /// The target again, when it is exactly our own `TreeBuilder`, so a /// per-event dispatch is one clone rather than a type check. native_target: Option>, - entity: Option, /// Cache of raw expat names to their `{uri}local` form. names: Option, handle_start: Option, @@ -1855,6 +1853,10 @@ pub(crate) mod _elementtree { )] #[derive(Debug, PyPayload)] pub(crate) struct PyXMLParser { + #[pymember] + entity: crate::vm::object::PyAtomicRef>, + #[pymember] + target: crate::vm::object::PyAtomicRef>, state: PyRwLock, } @@ -1865,9 +1867,9 @@ pub(crate) mod _elementtree { return; }; st.parser.traverse(traverse_fn); - st.target.traverse(traverse_fn); st.native_target.traverse(traverse_fn); - st.entity.traverse(traverse_fn); + self.entity.traverse(traverse_fn); + self.target.traverse(traverse_fn); st.names.traverse(traverse_fn); st.handle_start.traverse(traverse_fn); st.handle_end.traverse(traverse_fn); @@ -1887,9 +1889,9 @@ pub(crate) mod _elementtree { out.extend( [ st.parser.take(), - st.target.take(), + unsafe { self.entity.swap(None) }, + unsafe { self.target.swap(None) }, st.native_target.take().map(Into::into), - st.entity.take().map(Into::into), st.names.take().map(Into::into), st.handle_start.take(), st.handle_end.take(), @@ -1920,6 +1922,8 @@ pub(crate) mod _elementtree { impl Default for PyXMLParser { fn default() -> Self { Self { + entity: crate::vm::object::PyAtomicRef::from(None), + target: crate::vm::object::PyAtomicRef::from(None), state: PyRwLock::new(XMLParserState::default()), } } @@ -1963,9 +1967,11 @@ pub(crate) mod _elementtree { _ => PyTreeBuilder::default().into_ref(&vm.ctx).into(), }; + let entity = vm.ctx.new_dict(); + let _previous = unsafe { zelf.entity.swap(Some(entity.into())) }; + let _previous = unsafe { zelf.target.swap(Some(target.clone())) }; let handlers = XMLParserState { parser: Some(parser.clone()), - entity: Some(vm.ctx.new_dict()), names: Some(vm.ctx.new_dict()), handle_start_ns: optional_handler(&target, "start_ns", vm)?, handle_end_ns: optional_handler(&target, "end_ns", vm)?, @@ -1981,7 +1987,6 @@ pub(crate) mod _elementtree { .is(PyTreeBuilder::class(&vm.ctx)) .then(|| target.clone().downcast::().ok()) .flatten(), - target: Some(target), }; let has_comment = handlers.handle_comment.is_some(); let has_pi = handlers.handle_pi.is_some(); @@ -2044,7 +2049,7 @@ pub(crate) mod _elementtree { impl PyXMLParser { fn check(&self, vm: &VirtualMachine) -> PyResult { let st = self.state.read(); - if st.target.is_none() { + if self.target.deref().is_none() { return Err(vm.new_value_error("XMLParser.__init__() wasn't called")); } st.parser @@ -2125,24 +2130,6 @@ pub(crate) mod _elementtree { #[pyclass(with(Constructor, Initializer), flags(BASETYPE, HAS_WEAKREF))] impl PyXMLParser { - #[pygetset] - fn entity(&self, vm: &VirtualMachine) -> PyObjectRef { - self.state - .read() - .entity - .clone() - .map_or_else(|| vm.ctx.none(), Into::into) - } - - #[pygetset] - fn target(&self, vm: &VirtualMachine) -> PyObjectRef { - self.state - .read() - .target - .clone() - .unwrap_or_else(|| vm.ctx.none()) - } - #[pygetset] fn version(&self, vm: &VirtualMachine) -> PyResult { let expat = vm.import("pyexpat", 0)?; diff --git a/crates/stdlib/src/lzma.rs b/crates/stdlib/src/lzma.rs index 3958e90c0..69a360912 100644 --- a/crates/stdlib/src/lzma.rs +++ b/crates/stdlib/src/lzma.rs @@ -6,9 +6,11 @@ pub(crate) use _lzma::module_def; mod _lzma { use crate::compression::DecompressorArgs; use alloc::fmt; + use core::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use rustpython_common::{compression::lzma as backend, lock::PyMutex}; - use rustpython_vm::builtins::{PyBaseExceptionRef, PyBytesRef, PyDict, PyType, PyTypeRef}; + use rustpython_vm::builtins::{PyBaseExceptionRef, PyBytes, PyDict, PyType, PyTypeRef}; use rustpython_vm::function::ArgBytesLike; + use rustpython_vm::object::PyAtomicRef; use rustpython_vm::types::Constructor; use rustpython_vm::{Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine}; @@ -264,22 +266,25 @@ mod _lzma { struct DecompressorInner { backend: backend::Decompressor, - unused_data: PyBytesRef, - } - - impl DecompressorInner { - fn sync_visible_state(&mut self, vm: &VirtualMachine) { - if self.unused_data.as_bytes() != self.backend.unused_data() { - self.unused_data = vm.ctx.new_bytes(self.backend.unused_data().to_vec()); - } - } } #[pyattr] - #[pyclass(name = "LZMADecompressor")] + #[pyclass(name = "LZMADecompressor", traverse)] #[derive(PyPayload)] struct LZMADecompressor { + #[pytraverse(skip)] state: PyMutex, + #[pymember] + #[pytraverse(skip)] + check: AtomicI32, + #[pymember] + #[pytraverse(skip)] + eof: AtomicBool, + #[pymember] + #[pytraverse(skip)] + needs_input: AtomicBool, + #[pymember(type = "object_ex")] + unused_data: PyAtomicRef>, } impl fmt::Debug for LZMADecompressor { @@ -314,11 +319,13 @@ mod _lzma { let filters = filters_to_backend(args.filters, vm)?; let backend = backend::Decompressor::new(args.format, args.memlimit, filters) .map_err(|error| map_backend_error(error, vm))?; + let check = backend.check(); Ok(Self { - state: PyMutex::new(DecompressorInner { - backend, - unused_data: vm.ctx.empty_bytes.clone(), - }), + state: PyMutex::new(DecompressorInner { backend }), + check: AtomicI32::new(check), + eof: AtomicBool::new(false), + needs_input: AtomicBool::new(true), + unused_data: PyAtomicRef::from(Some(vm.ctx.empty_bytes.clone().into())), }) } } @@ -331,29 +338,20 @@ mod _lzma { let data = &*args.data(); let mut state = self.state.lock(); let result = state.backend.decompress(data, max_length); - state.sync_visible_state(vm); + self.check.store(state.backend.check(), Ordering::Relaxed); + self.eof.store(state.backend.eof(), Ordering::Relaxed); + self.needs_input + .store(state.backend.needs_input(), Ordering::Relaxed); + let stale = self.unused_data.deref().is_none_or(|obj| { + obj.downcast_ref::() + .is_none_or(|bytes| bytes.as_bytes() != state.backend.unused_data()) + }); + if stale { + let bytes = vm.ctx.new_bytes(state.backend.unused_data().to_vec()); + let _previous = unsafe { self.unused_data.swap(Some(bytes.into())) }; + } result.map_err(|error| map_backend_error(error, vm)) } - - #[pygetset] - fn check(&self) -> i32 { - self.state.lock().backend.check() - } - - #[pygetset] - fn eof(&self) -> bool { - self.state.lock().backend.eof() - } - - #[pygetset] - fn unused_data(&self) -> PyBytesRef { - self.state.lock().unused_data.clone() - } - - #[pygetset] - fn needs_input(&self) -> bool { - self.state.lock().backend.needs_input() - } } #[pyattr] diff --git a/crates/stdlib/src/multiprocessing.rs b/crates/stdlib/src/multiprocessing.rs index 1c98e7e0e..f8b11e46a 100644 --- a/crates/stdlib/src/multiprocessing.rs +++ b/crates/stdlib/src/multiprocessing.rs @@ -1,5 +1,73 @@ pub(crate) use _multiprocessing::module_def; +#[cfg(any(unix, windows))] +const _: () = assert!( + core::mem::size_of::() + == core::mem::size_of::() + && core::mem::align_of::() + == core::mem::align_of::() +); + +/// `SemLock.handle` is one pointer-sized word (`sem_t *` or `HANDLE`). +#[cfg(any(unix, windows))] +#[repr(transparent)] +struct SemHandleWord(rustpython_host_env::multiprocessing::SemHandle); + +#[cfg(any(unix, windows))] +impl core::ops::Deref for SemHandleWord { + type Target = rustpython_host_env::multiprocessing::SemHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(any(unix, windows))] +impl core::fmt::Debug for SemHandleWord { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(any(unix, windows))] +impl crate::vm::builtins::descriptor::MemberLayout for SemHandleWord { + const KIND: crate::vm::builtins::descriptor::MemberKind = { + if cfg!(all(windows, target_pointer_width = "64")) { + crate::vm::builtins::descriptor::MemberKind::ULongLong + } else { + crate::vm::builtins::descriptor::MemberKind::ULong + } + }; +} + +#[cfg(any(unix, windows))] +fn semlock_name( + name: Option, +) -> ( + Option, + crate::vm::builtins::descriptor::CStrMember, +) { + let Some(name) = name else { + return ( + None, + crate::vm::builtins::descriptor::CStrMember::new(core::ptr::null()), + ); + }; + let owner = match alloc::ffi::CString::new(name) { + Ok(owner) => owner, + Err(err) => { + let bytes: Vec = err + .into_vec() + .into_iter() + .filter(|byte| *byte != 0) + .collect(); + alloc::ffi::CString::new(bytes).unwrap_or_default() + } + }; + let member = crate::vm::builtins::descriptor::CStrMember::new(owner.as_ptr()); + (Some(owner), member) +} + #[cfg(windows)] #[pymodule] mod _multiprocessing { @@ -63,10 +131,17 @@ mod _multiprocessing { #[pyclass(name = "SemLock", module = "_multiprocessing")] #[derive(Debug, PyPayload)] struct SemLock { - handle: SemHandle, + #[pymember] + handle: super::SemHandleWord, + #[pymember] kind: i32, + #[pymember] maxvalue: i32, - name: Option, + /// Owns the bytes `name` points at. + #[expect(dead_code, reason = "keeps the semaphore name allocation alive")] + name_owner: Option, + #[pymember] + name: crate::vm::builtins::descriptor::CStrMember, last_tid: AtomicU32, count: AtomicI32, /// Serializes owner bookkeeping. Dropped around blocking waits. @@ -77,26 +152,6 @@ mod _multiprocessing { #[pyclass(with(Constructor), flags(BASETYPE))] impl SemLock { - #[pygetset] - fn handle(&self) -> isize { - self.handle.as_handle_int() - } - - #[pygetset] - fn kind(&self) -> i32 { - self.kind - } - - #[pygetset] - fn maxvalue(&self) -> i32 { - self.maxvalue - } - - #[pygetset] - fn name(&self) -> Option { - self.name.clone() - } - #[pymethod] fn acquire(&self, args: AcquireArgs, vm: &VirtualMachine) -> PyResult { let blocking = args.block; @@ -237,11 +292,15 @@ mod _multiprocessing { vm: &VirtualMachine, ) -> PyResult { // On Windows, _rebuild receives the handle directly (no sem_open) + let (name_owner, name_member) = super::semlock_name(name); let zelf = Self { - handle: SemHandle::from_raw(handle as host_multiprocessing::RawHandle), + handle: super::SemHandleWord(SemHandle::from_raw( + handle as host_multiprocessing::RawHandle, + )), kind, maxvalue, - name, + name_owner, + name: name_member, last_tid: AtomicU32::new(0), count: AtomicI32::new(0), crit: PyMutex::new(()), @@ -311,14 +370,17 @@ mod _multiprocessing { return Err(vm.new_value_error("invalid value")); } - let handle = - SemHandle::create(args.value, args.maxvalue).map_err(|e| e.to_pyexception(vm))?; - let name = if args.unlink { None } else { Some(args.name) }; + let handle = super::SemHandleWord( + SemHandle::create(args.value, args.maxvalue).map_err(|e| e.to_pyexception(vm))?, + ); + let (name_owner, name) = + super::semlock_name(if args.unlink { None } else { Some(args.name) }); Ok(Self { handle, kind: args.kind, maxvalue: args.maxvalue, + name_owner, name, last_tid: AtomicU32::new(0), count: AtomicI32::new(0), @@ -462,10 +524,17 @@ mod _multiprocessing { #[pyclass(name = "SemLock", module = "_multiprocessing")] #[derive(Debug, PyPayload)] struct SemLock { - handle: SemHandle, + #[pymember] + handle: super::SemHandleWord, + #[pymember] kind: i32, + #[pymember] maxvalue: i32, - name: Option, + /// Owns the bytes `name` points at. + #[expect(dead_code, reason = "keeps the semaphore name allocation alive")] + name_owner: Option, + #[pymember] + name: crate::vm::builtins::descriptor::CStrMember, last_tid: AtomicU64, // unsigned long count: AtomicI32, // int /// Serializes owner bookkeeping. Dropped around blocking waits. @@ -476,26 +545,6 @@ mod _multiprocessing { #[pyclass(with(Constructor), flags(BASETYPE))] impl SemLock { - #[pygetset] - fn handle(&self) -> isize { - self.handle.as_handle_int() - } - - #[pygetset] - fn kind(&self) -> i32 { - self.kind - } - - #[pygetset] - fn maxvalue(&self) -> i32 { - self.maxvalue - } - - #[pygetset] - fn name(&self) -> Option { - self.name.clone() - } - // _multiprocessing_SemLock_acquire_impl #[pymethod] fn acquire(&self, args: AcquireArgs, vm: &VirtualMachine) -> PyResult { @@ -701,12 +750,16 @@ mod _multiprocessing { let Some(ref name_str) = name else { return Err(vm.new_value_error("cannot rebuild SemLock without name")); }; - let handle = SemHandle::open_existing(name_str).map_err(|err| os_error(vm, err))?; + let handle = super::SemHandleWord( + SemHandle::open_existing(name_str).map_err(|err| os_error(vm, err))?, + ); // return newsemlockobject(type, handle, kind, maxvalue, name_copy); + let (name_owner, name) = super::semlock_name(name); let zelf = Self { handle, kind, maxvalue, + name_owner, name, last_tid: AtomicU64::new(0), count: AtomicI32::new(0), @@ -828,10 +881,12 @@ mod _multiprocessing { })?; // return newsemlockobject(type, handle, kind, maxvalue, name_copy); + let (name_owner, name) = super::semlock_name(name); Ok(Self { - handle, + handle: super::SemHandleWord(handle), kind: args.kind, maxvalue: args.maxvalue, + name_owner, name, last_tid: AtomicU64::new(0), count: AtomicI32::new(0), diff --git a/crates/stdlib/src/pickle.rs b/crates/stdlib/src/pickle.rs index 9fbf73498..5ee195078 100644 --- a/crates/stdlib/src/pickle.rs +++ b/crates/stdlib/src/pickle.rs @@ -18,6 +18,7 @@ mod _pickle { protocol::{PyBuffer, PyIter, PyIterReturn}, types::{AsBuffer, Constructor, Initializer, Representable}, }; + use core::sync::atomic::{AtomicI32, Ordering}; use malachite_bigint::BigInt; use num_traits::{ToPrimitive, Zero}; use std::collections::HashMap; @@ -1872,8 +1873,6 @@ mod _pickle { pub(super) struct PicklerConfig { initialized: bool, proto: u8, - bin: bool, - fast: bool, fix_imports: bool, buffer_callback: Option, } @@ -1883,8 +1882,6 @@ mod _pickle { Self { initialized: false, proto: DEFAULT_PROTOCOL, - bin: true, - fast: false, fix_imports: false, buffer_callback: None, } @@ -1943,6 +1940,10 @@ mod _pickle { out: PyMutex, memo: PyRwLock, config: PyRwLock, + #[pymember(writable)] + bin: AtomicI32, + #[pymember(writable)] + fast: AtomicI32, } #[derive(FromArgs)] @@ -1989,8 +1990,6 @@ mod _pickle { Ok(PicklerConfig { initialized: true, proto, - bin: proto >= 1, - fast: false, fix_imports: args.fix_imports && proto < 3, buffer_callback, }) @@ -2004,6 +2003,8 @@ mod _pickle { out: PyMutex::new(Output::default()), memo: PyRwLock::new(MemoTable::default()), config: PyRwLock::new(PicklerConfig::default()), + bin: AtomicI32::new(1), + fast: AtomicI32::new(0), }) } } @@ -2028,6 +2029,9 @@ mod _pickle { ..Output::default() }; zelf.memo.write().clear(); + zelf.bin + .store(i32::from(config.proto >= 1), Ordering::Relaxed); + zelf.fast.store(0, Ordering::Relaxed); *zelf.config.write() = config; Ok(()) } @@ -2095,42 +2099,6 @@ mod _pickle { *zelf.memo.write() = new_memo; Ok(()) } - - #[pygetset] - fn fast(&self) -> bool { - self.config.read().fast - } - - #[pygetset(setter)] - fn set_fast(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { - match value { - PySetterValue::Assign(v) => { - self.config.write().fast = v.try_to_bool(vm)?; - Ok(()) - } - PySetterValue::Delete => { - Err(vm.new_type_error("attribute deletion is not supported")) - } - } - } - - #[pygetset] - fn bin(&self) -> bool { - self.config.read().bin - } - - #[pygetset(setter)] - fn set_bin(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { - match value { - PySetterValue::Assign(v) => { - self.config.write().bin = v.try_to_bool(vm)?; - Ok(()) - } - PySetterValue::Delete => { - Err(vm.new_type_error("attribute deletion is not supported")) - } - } - } } // saving @@ -3709,17 +3677,17 @@ mod _pickle { let Some(mut out) = zelf.out.try_lock() else { return Err(vm.new_runtime_error("Pickler.dump() called recursively")); }; - let (initialized, proto, bin, fast, fix_imports, buffer_callback) = { + let (initialized, proto, fix_imports, buffer_callback) = { let cfg = zelf.config.read(); ( cfg.initialized, cfg.proto, - cfg.bin, - cfg.fast, cfg.fix_imports, cfg.buffer_callback.clone(), ) }; + let bin = zelf.bin.load(Ordering::Relaxed) != 0; + let fast = zelf.fast.load(Ordering::Relaxed) != 0; if !initialized { return Err(new_pickling_error( vm, @@ -3803,6 +3771,8 @@ mod _pickle { ..Output::default() }), memo: PyRwLock::new(MemoTable::default()), + bin: AtomicI32::new(i32::from(config.proto >= 1)), + fast: AtomicI32::new(0), config: PyRwLock::new(config), } .into_ref(&vm.ctx); @@ -3833,6 +3803,8 @@ mod _pickle { let pickler = PyPickler { out: PyMutex::new(Output::default()), memo: PyRwLock::new(MemoTable::default()), + bin: AtomicI32::new(i32::from(config.proto >= 1)), + fast: AtomicI32::new(0), config: PyRwLock::new(config), } .into_ref(&vm.ctx); diff --git a/crates/stdlib/src/pyexpat.rs b/crates/stdlib/src/pyexpat.rs index 5f8c4f6af..873a02a46 100644 --- a/crates/stdlib/src/pyexpat.rs +++ b/crates/stdlib/src/pyexpat.rs @@ -211,7 +211,7 @@ mod _pyexpat { #[pyattr] #[pyattr(name = "XMLParserType")] - #[pyclass(name = "xmlparser", module = false, traverse)] + #[pyclass(name = "xmlparser", traverse)] #[derive(Debug, PyPayload)] pub(super) struct PyExpatLikeXmlParser { #[pytraverse(skip)] @@ -255,7 +255,8 @@ mod _pyexpat { namespace_prefixes: MutableObject, ordered_attributes: MutableObject, specified_attributes: MutableObject, - intern: MutableObject, + #[pymember] + intern: PyObjectRef, // Additional handlers (stubs for compatibility) processing_instruction: MutableObject, unparsed_entity_decl: MutableObject, @@ -830,7 +831,7 @@ mod _pyexpat { namespace_prefixes: MutableObject::new(vm.ctx.new_bool(false).into()), ordered_attributes: MutableObject::new(vm.ctx.new_bool(false).into()), specified_attributes: MutableObject::new(vm.ctx.new_bool(false).into()), - intern: MutableObject::new(intern_dict), + intern: intern_dict, // Additional handlers (stubs for compatibility) processing_instruction: MutableObject::new(vm.ctx.none()), unparsed_entity_decl: MutableObject::new(vm.ctx.none()), @@ -890,7 +891,6 @@ mod _pyexpat { class, specified_attributes ); - create_property!(ctx, attributes, "intern", class, intern); create_readonly_int_property!( ctx, attributes, @@ -1066,7 +1066,7 @@ mod _pyexpat { /// Attribute *values* are intentionally left out of this cache, same /// as libexpat: they vary far more than names and rarely repeat. fn intern_name(&self, vm: &VirtualMachine, name: String) -> PyStrRef { - let intern_obj = self.intern.read().clone(); + let intern_obj = self.intern.clone(); if let Ok(dict) = intern_obj.downcast::() { if let Ok(Some(existing)) = dict.get_item_opt(name.as_str(), vm) && let Ok(existing) = existing.downcast::() @@ -1460,7 +1460,7 @@ mod _pyexpat { // TODO: Tie this exception to the module's state. #[pyattr] #[pyattr(name = "error")] - #[pyexception(name = "ExpatError", base = PyException)] + #[pyexception(name = "ExpatError", module = "xml.parsers.expat", base = PyException)] #[derive(Debug)] #[repr(transparent)] pub(super) struct PyExpatError(PyException); diff --git a/crates/stdlib/src/select.rs b/crates/stdlib/src/select.rs index 7794496b2..d55fe8312 100644 --- a/crates/stdlib/src/select.rs +++ b/crates/stdlib/src/select.rs @@ -692,22 +692,42 @@ mod decl { Py, PyObject, PyPayload, PyRef, builtins::{PyFloat, PyType}, class_or_notimplemented, - common::lock::{PyMutex, PyRwLock}, + common::lock::PyRwLock, convert::{IntoPyException, ToPyObject}, function::PyComparisonValue, types::{Comparable, Constructor, Destructor, PyComparisonOp, Representable}, }; use alloc::sync::Arc; - use core::sync::atomic::AtomicI32; + use core::sync::atomic::{ + AtomicI16, AtomicI32, AtomicI64, AtomicU16, AtomicU32, AtomicU64, Ordering, + }; use num_traits::ToPrimitive; use std::time::Instant; #[pyclass(module = "select", name = "kevent")] #[derive(Debug, PyPayload)] pub(crate) struct PyKevent { - ev: PyMutex, + #[pymember(writable)] + ident: AtomicU64, + #[pymember(writable)] + filter: AtomicI16, + #[pymember(writable)] + flags: AtomicU16, + #[pymember(writable)] + fflags: AtomicU32, + #[pymember(writable)] + data: AtomicI64, + #[pymember(writable)] + udata: AtomicU64, } + const _: () = assert!( + core::mem::size_of::() == core::mem::size_of::() + && core::mem::size_of::() == core::mem::size_of::() + && core::mem::size_of::() == core::mem::size_of::() + && core::mem::size_of::() == core::mem::size_of::() + ); + #[derive(FromArgs)] pub(crate) struct KeventNewArgs { #[pyarg(any)] @@ -741,14 +761,12 @@ mod decl { fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { let ident = ident_from_object(&args.ident, vm)?; Ok(Self { - ev: PyMutex::new(host_select::kqueue::Event { - ident, - filter: args.filter, - flags: args.flags, - fflags: args.fflags, - data: args.data, - udata: args.udata, - }), + ident: AtomicU64::new(ident as u64), + filter: AtomicI16::new(args.filter), + flags: AtomicU16::new(args.flags), + fflags: AtomicU32::new(args.fflags), + data: AtomicI64::new(args.data as i64), + udata: AtomicU64::new(args.udata as u64), }) } } @@ -756,75 +774,26 @@ mod decl { #[pyclass(with(Constructor, Comparable, Representable))] impl PyKevent { pub(super) fn event(&self) -> host_select::kqueue::Event { - *self.ev.lock() + host_select::kqueue::Event { + ident: self.ident.load(Ordering::Relaxed) as usize, + filter: self.filter.load(Ordering::Relaxed), + flags: self.flags.load(Ordering::Relaxed), + fflags: self.fflags.load(Ordering::Relaxed), + data: self.data.load(Ordering::Relaxed) as isize, + udata: self.udata.load(Ordering::Relaxed) as usize, + } } pub(super) fn from_event(ev: host_select::kqueue::Event) -> Self { Self { - ev: PyMutex::new(ev), + ident: AtomicU64::new(ev.ident as u64), + filter: AtomicI16::new(ev.filter), + flags: AtomicU16::new(ev.flags), + fflags: AtomicU32::new(ev.fflags), + data: AtomicI64::new(ev.data as i64), + udata: AtomicU64::new(ev.udata as u64), } } - - #[pygetset] - fn ident(&self) -> usize { - self.ev.lock().ident - } - - #[pygetset(setter)] - fn set_ident(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - self.ev.lock().ident = ident_from_object(&value, vm)?; - Ok(()) - } - - #[pygetset] - fn filter(&self) -> i16 { - self.ev.lock().filter - } - - #[pygetset(setter)] - fn set_filter(&self, value: i16) { - self.ev.lock().filter = value; - } - - #[pygetset] - fn flags(&self) -> u16 { - self.ev.lock().flags - } - - #[pygetset(setter)] - fn set_flags(&self, value: u16) { - self.ev.lock().flags = value; - } - - #[pygetset] - fn fflags(&self) -> u32 { - self.ev.lock().fflags - } - - #[pygetset(setter)] - fn set_fflags(&self, value: u32) { - self.ev.lock().fflags = value; - } - - #[pygetset] - fn data(&self) -> isize { - self.ev.lock().data - } - - #[pygetset(setter)] - fn set_data(&self, value: isize) { - self.ev.lock().data = value; - } - - #[pygetset] - fn udata(&self) -> usize { - self.ev.lock().udata - } - - #[pygetset(setter)] - fn set_udata(&self, value: usize) { - self.ev.lock().udata = value; - } } impl Comparable for PyKevent { diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 283ccce57..d6cbaae44 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -44,6 +44,7 @@ mod _socket { Ok(()) } + use core::sync::atomic::{AtomicI32, Ordering}; use core::{ mem::ManuallyDrop, net::{Ipv4Addr, Ipv6Addr, SocketAddr}, @@ -1062,9 +1063,12 @@ mod _socket { #[pyclass(name = "socket")] #[derive(Debug, PyPayload)] pub struct PySocket { - kind: AtomicCell, - family: AtomicCell, - proto: AtomicCell, + #[pymember(name = "type")] + kind: AtomicI32, + #[pymember] + family: AtomicI32, + #[pymember] + proto: AtomicI32, pub(crate) timeout: AtomicCell, sock: PyRwLock>, } @@ -1074,9 +1078,9 @@ mod _socket { impl Default for PySocket { fn default() -> Self { Self { - kind: AtomicCell::default(), - family: AtomicCell::default(), - proto: AtomicCell::default(), + kind: AtomicI32::new(0), + family: AtomicI32::new(0), + proto: AtomicI32::new(0), timeout: AtomicCell::new(-1.0), sock: PyRwLock::new(None), } @@ -1137,7 +1141,7 @@ mod _socket { proto: i32, sock: Socket, ) -> io::Result<()> { - self.family.store(family); + self.family.store(family, Ordering::Relaxed); // Mask out SOCK_NONBLOCK and SOCK_CLOEXEC flags from stored type // to ensure consistent cross-platform behavior #[cfg(any( @@ -1164,8 +1168,8 @@ mod _socket { target_os = "redox" )))] let masked_kind = socket_kind; - self.kind.store(masked_kind); - self.proto.store(proto); + self.kind.store(masked_kind, Ordering::Relaxed); + self.proto.store(proto, Ordering::Relaxed); let mut s = self.sock.write(); let sock = s.insert(sock); // If SOCK_NONBLOCK is set, use timeout 0 (non-blocking) @@ -1272,7 +1276,7 @@ mod _socket { caller: &str, vm: &VirtualMachine, ) -> Result { - let family = self.family.load(); + let family = self.family.load(Ordering::Relaxed); match family { #[cfg(unix)] c::AF_UNIX => { @@ -1338,11 +1342,11 @@ mod _socket { } #[cfg(windows)] family if family == c::AF_HYPERV => { - if self.proto.load() != host_socket::HV_PROTOCOL_RAW { + if self.proto.load(Ordering::Relaxed) != host_socket::HV_PROTOCOL_RAW { return Err(vm .new_os_error(format!( "{caller}(): unsupported AF_HYPERV protocol: {}", - self.proto.load() + self.proto.load(Ordering::Relaxed) )) .into()); } @@ -1403,7 +1407,7 @@ mod _socket { } #[cfg(windows)] family if family == c::AF_BLUETOOTH => { - if self.proto.load() != host_socket::BTHPROTO_RFCOMM { + if self.proto.load(Ordering::Relaxed) != host_socket::BTHPROTO_RFCOMM { return Err(vm .new_os_error(format!("{caller}(): unknown Bluetooth protocol")) .into()); @@ -1448,7 +1452,7 @@ mod _socket { obj.class().name() )) })?; - let proto = self.proto.load(); + let proto = self.proto.load(Ordering::Relaxed); let interface: PyStrRef = tuple .as_slice() .first() @@ -1697,9 +1701,9 @@ mod _socket { Ok(format!( "", zelf.fileno(), - zelf.family.load(), - zelf.kind.load(), - zelf.proto.load(), + zelf.family.load(Ordering::Relaxed), + zelf.kind.load(Ordering::Relaxed), + zelf.proto.load(Ordering::Relaxed), )) } } @@ -1896,7 +1900,10 @@ mod _socket { self.sock_snapshot()?.accept_raw() })?; let fd = into_sock_fileno(sock); - Ok((fd, get_addr_tuple(&addr, self.proto.load(), vm))) + Ok(( + fd, + get_addr_tuple(&addr, self.proto.load(Ordering::Relaxed), vm), + )) } #[pymethod] @@ -1969,7 +1976,10 @@ mod _socket { .recv_from_with_flags(buffer.spare_capacity_mut(), flags) })?; unsafe { buffer.set_len(n) }; - Ok((buffer, get_addr_tuple(&addr, self.proto.load(), vm))) + Ok(( + buffer, + get_addr_tuple(&addr, self.proto.load(Ordering::Relaxed), vm), + )) } #[pymethod] @@ -2002,7 +2012,10 @@ mod _socket { })?; unsafe { scratch.set_len(n) }; buf.borrow_buf_mut()[..n].copy_from_slice(&scratch); - Ok((n, get_addr_tuple(&addr, self.proto.load(), vm))) + Ok(( + n, + get_addr_tuple(&addr, self.proto.load(Ordering::Relaxed), vm), + )) } #[pymethod] @@ -2226,7 +2239,7 @@ mod _socket { let storage: host_socket::raw::SockAddrStorage = unsafe { core::mem::transmute(address.storage) }; let addr = unsafe { host_socket::raw::SockAddr::new(storage, address.len as _) }; - get_addr_tuple(&addr, self.proto.load(), vm) + get_addr_tuple(&addr, self.proto.load(Ordering::Relaxed), vm) } else { vm.ctx.none() }; @@ -2310,14 +2323,22 @@ mod _socket { fn getsockname(&self, vm: &VirtualMachine) -> std::io::Result { let addr = self.sock()?.local_addr()?; - Ok(get_addr_tuple(&addr, self.proto.load(), vm)) + Ok(get_addr_tuple( + &addr, + self.proto.load(Ordering::Relaxed), + vm, + )) } #[pymethod] fn getpeername(&self, vm: &VirtualMachine) -> std::io::Result { let addr = self.sock()?.peer_addr()?; - Ok(get_addr_tuple(&addr, self.proto.load(), vm)) + Ok(get_addr_tuple( + &addr, + self.proto.load(Ordering::Relaxed), + vm, + )) } #[pymethod] @@ -2507,21 +2528,6 @@ mod _socket { let fd = sock_fileno(&sock); host_socket::share_socket(fd as _, process_id).map_err(Into::into) } - - #[pygetset(name = "type")] - fn kind(&self) -> i32 { - self.kind.load() - } - - #[pygetset] - fn family(&self) -> i32 { - self.family.load() - } - - #[pygetset] - fn proto(&self) -> i32 { - self.proto.load() - } } struct Address { diff --git a/crates/stdlib/src/socket_wasm.rs b/crates/stdlib/src/socket_wasm.rs index 785b63fd2..b6bf28eb0 100644 --- a/crates/stdlib/src/socket_wasm.rs +++ b/crates/stdlib/src/socket_wasm.rs @@ -207,8 +207,11 @@ mod _socket { #[pyclass(name = "socket")] #[derive(Debug, PyPayload)] struct PySocket { + #[pymember] family: AtomicI32, + #[pymember(name = "type")] kind: AtomicI32, + #[pymember] proto: AtomicI32, timeout: PyMutex>, closed: PyMutex, @@ -235,21 +238,6 @@ mod _socket { Ok(()) } - #[pygetset] - fn family(&self) -> i32 { - self.family.load(Ordering::Relaxed) - } - - #[pygetset] - fn r#type(&self) -> i32 { - self.kind.load(Ordering::Relaxed) - } - - #[pygetset] - fn proto(&self) -> i32 { - self.proto.load(Ordering::Relaxed) - } - #[pymethod] fn fileno(&self) -> i32 { if *self.closed.lock() { -1 } else { 0 } diff --git a/crates/stdlib/src/ssl/error.rs b/crates/stdlib/src/ssl/error.rs index 041b22b64..53533c41f 100644 --- a/crates/stdlib/src/ssl/error.rs +++ b/crates/stdlib/src/ssl/error.rs @@ -18,7 +18,7 @@ pub(crate) mod ssl_error { }; #[pyattr] - #[pyexception(name = "SSLError", base = PyOSError)] + #[pyexception(name = "SSLError", module = "ssl", base = PyOSError)] #[derive(Debug)] #[repr(transparent)] pub(crate) struct PySSLError(PyOSError); @@ -47,7 +47,7 @@ pub(crate) mod ssl_error { } #[pyattr] - #[pyexception(name = "SSLZeroReturnError", base = PySSLError)] + #[pyexception(name = "SSLZeroReturnError", module = "ssl", base = PySSLError)] #[derive(Debug)] #[repr(transparent)] pub(crate) struct PySSLZeroReturnError(PySSLError); @@ -56,31 +56,31 @@ pub(crate) mod ssl_error { impl PySSLZeroReturnError {} #[pyattr] - #[pyexception(name = "SSLWantReadError", base = PySSLError, impl)] + #[pyexception(name = "SSLWantReadError", module = "ssl", base = PySSLError, impl)] #[derive(Debug)] #[repr(transparent)] pub(crate) struct PySSLWantReadError(PySSLError); #[pyattr] - #[pyexception(name = "SSLWantWriteError", base = PySSLError, impl)] + #[pyexception(name = "SSLWantWriteError", module = "ssl", base = PySSLError, impl)] #[derive(Debug)] #[repr(transparent)] pub(crate) struct PySSLWantWriteError(PySSLError); #[pyattr] - #[pyexception(name = "SSLSyscallError", base = PySSLError, impl)] + #[pyexception(name = "SSLSyscallError", module = "ssl", base = PySSLError, impl)] #[derive(Debug)] #[repr(transparent)] pub(crate) struct PySSLSyscallError(PySSLError); #[pyattr] - #[pyexception(name = "SSLEOFError", base = PySSLError, impl)] + #[pyexception(name = "SSLEOFError", module = "ssl", base = PySSLError, impl)] #[derive(Debug)] #[repr(transparent)] pub(crate) struct PySSLEOFError(PySSLError); #[pyattr] - #[pyexception(name = "SSLCertVerificationError", base = PySSLError, impl)] + #[pyexception(name = "SSLCertVerificationError", module = "ssl", base = PySSLError, impl)] #[derive(Debug)] #[repr(transparent)] pub(crate) struct PySSLCertVerificationError(PySSLError); diff --git a/crates/stdlib/src/unicodedata.rs b/crates/stdlib/src/unicodedata.rs index fd74e73d0..e2d9883c2 100644 --- a/crates/stdlib/src/unicodedata.rs +++ b/crates/stdlib/src/unicodedata.rs @@ -72,12 +72,27 @@ mod unicodedata { #[derive(Debug, PyPayload)] pub(super) struct Ucd { inner: unicode_core::Ucd, + /// Owns the bytes `unidata_version` points at. + #[expect(dead_code, reason = "keeps the version string alive")] + version_owner: alloc::ffi::CString, + #[pymember] + unidata_version: crate::vm::builtins::descriptor::CStrMember, } impl Ucd { - pub(super) const fn new(modern: bool) -> Self { + pub(super) fn new(modern: bool) -> Self { + let text = if modern { + unicode_core::unicode_version() + } else { + "3.2.0".to_owned() + }; + let version_owner = alloc::ffi::CString::new(text).unwrap_or_default(); + let unidata_version = + crate::vm::builtins::descriptor::CStrMember::new(version_owner.as_ptr()); Self { inner: unicode_core::Ucd::new(modern), + version_owner, + unidata_version, } } @@ -217,11 +232,6 @@ mod unicodedata { .map(Option::Some) .ok_or_else(|| vm.new_value_error("not a numeric character")) } - - #[pygetset] - fn unidata_version(&self) -> String { - self.inner.unidata_version() - } } #[pyattr] diff --git a/crates/stdlib/src/zlib.rs b/crates/stdlib/src/zlib.rs index d068b02a6..ca5df00b6 100644 --- a/crates/stdlib/src/zlib.rs +++ b/crates/stdlib/src/zlib.rs @@ -7,14 +7,16 @@ mod zlib { use crate::compression::{DecompressArgs, DecompressorArgs}; use crate::vm::{ Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, - builtins::{PyBaseExceptionRef, PyBytesRef, PyType, PyTypeRef}, + builtins::{PyBaseExceptionRef, PyBytes, PyBytesRef, PyType, PyTypeRef}, common::lock::PyMutex, convert::TryFromBorrowedObject, function::{ArgBytesLike, ArgIndex, OptionalArg, PySize, PySsize}, + object::PyAtomicRef, types::Constructor, }; use adler32::RollingAdler32 as Adler32; use alloc::fmt; + use core::sync::atomic::{AtomicBool, Ordering}; use rustpython_common::compression::zlib as backend; #[pyattr] @@ -416,7 +418,6 @@ mod zlib { struct PyZlibDecompressorInner { decompress: backend::ZlibDecompressor, - unused_data: PyBytesRef, } #[pyattr] @@ -425,6 +426,14 @@ mod zlib { struct ZlibDecompressor { #[pytraverse(skip)] inner: PyMutex, + #[pymember] + #[pytraverse(skip)] + eof: AtomicBool, + #[pymember] + #[pytraverse(skip)] + needs_input: AtomicBool, + #[pymember(type = "object_ex")] + unused_data: PyAtomicRef>, } impl fmt::Debug for ZlibDecompressor { @@ -450,31 +459,16 @@ mod zlib { let decompress = backend::ZlibDecompressor::new(args.wbits, owned_dict(args.zdict)) .map_err(|err| new_init_or_zlib_error(err, vm))?; Ok(Self { - inner: PyMutex::new(PyZlibDecompressorInner { - decompress, - unused_data: vm.ctx.empty_bytes.clone(), - }), + inner: PyMutex::new(PyZlibDecompressorInner { decompress }), + eof: AtomicBool::new(false), + needs_input: AtomicBool::new(true), + unused_data: PyAtomicRef::from(Some(vm.ctx.empty_bytes.clone().into())), }) } } #[pyclass(with(Constructor))] impl ZlibDecompressor { - #[pygetset] - fn eof(&self) -> bool { - self.inner.lock().decompress.eof() - } - - #[pygetset] - fn unused_data(&self) -> PyBytesRef { - self.inner.lock().unused_data.clone() - } - - #[pygetset] - fn needs_input(&self) -> bool { - self.inner.lock().decompress.needs_input() - } - #[pymethod] fn decompress(&self, args: DecompressorArgs, vm: &VirtualMachine) -> PyResult> { let max_length = args.max_length(); @@ -482,8 +476,16 @@ mod zlib { let mut inner = self.inner.lock(); let result = inner.decompress.decompress(data, max_length); - if inner.unused_data.as_bytes() != inner.decompress.unused_data() { - inner.unused_data = vm.ctx.new_bytes(inner.decompress.unused_data().to_vec()); + self.eof.store(inner.decompress.eof(), Ordering::Relaxed); + self.needs_input + .store(inner.decompress.needs_input(), Ordering::Relaxed); + let stale = self.unused_data.deref().is_none_or(|obj| { + obj.downcast_ref::() + .is_none_or(|bytes| bytes.as_bytes() != inner.decompress.unused_data()) + }); + if stale { + let bytes = vm.ctx.new_bytes(inner.decompress.unused_data().to_vec()); + let _previous = unsafe { self.unused_data.swap(Some(bytes.into())) }; } result.map_err(|err| match err { backend::DecompressError::Zlib(err) => new_zlib_error(err, vm), diff --git a/crates/vm/src/builtins/asyncgenerator.rs b/crates/vm/src/builtins/asyncgenerator.rs index 788725621..7ff13e4be 100644 --- a/crates/vm/src/builtins/asyncgenerator.rs +++ b/crates/vm/src/builtins/asyncgenerator.rs @@ -60,6 +60,7 @@ impl PyPayload for PyAsyncGen { } #[pyclass( + itemsize = core::mem::size_of::(), flags(DISALLOW_INSTANTIATION, HAS_WEAKREF), with(PyRef, Representable, Destructor) )] diff --git a/crates/vm/src/builtins/builtin_func.rs b/crates/vm/src/builtins/builtin_func.rs index 91ddf64c4..9747f4571 100644 --- a/crates/vm/src/builtins/builtin_func.rs +++ b/crates/vm/src/builtins/builtin_func.rs @@ -18,8 +18,8 @@ pub struct PyNativeFunction { pub(crate) zelf: Option, // Module that owns this function. Not passed as a call argument. pub(crate) module_object: Option, - #[pytraverse(skip)] - pub(crate) module: Option<&'static PyStrInterned>, // None for bound method + #[pymember(name = "__module__", writable)] + pub(crate) module: crate::object::PyAtomicRef>, /// Prevent HeapMethodDef from being freed while this function references it pub(crate) _method_def_owner: Option, } @@ -32,11 +32,18 @@ impl PyPayload for PyNativeFunction { impl fmt::Debug for PyNativeFunction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let module = match self + .module + .deref() + .and_then(|m| m.downcast_ref::()) + { + Some(module) => module.as_wtf8().to_owned(), + None => Wtf8::new("").to_owned(), + }; write!( f, "builtin function {}.{} ({:?}) self as instance of {:?}", - self.module - .map_or_else(|| Wtf8::new(""), |m| m.as_wtf8()), + module, self.value.name, self.value.flags, self.zelf.as_ref().map(|z| z.class().name().to_owned()) @@ -45,8 +52,8 @@ impl fmt::Debug for PyNativeFunction { } impl PyNativeFunction { - pub const fn with_module(mut self, module: &'static PyStrInterned) -> Self { - self.module = Some(module); + pub fn with_module(self, module: &'static PyStrInterned) -> Self { + drop(self.module.store(Some(module.to_owned().into()))); self } @@ -149,11 +156,6 @@ impl Representable for PyNativeFunction { flags(HAS_WEAKREF, DISALLOW_INSTANTIATION) )] impl PyNativeFunction { - #[pygetset] - fn __module__(zelf: NativeFunctionOrMethod) -> Option<&'static PyStrInterned> { - zelf.0.module - } - #[pygetset] fn __name__(zelf: NativeFunctionOrMethod) -> &'static str { zelf.0.value.name @@ -203,7 +205,7 @@ impl PyNativeFunction { #[pymethod] fn __reduce__(zelf: NativeFunctionOrMethod, vm: &VirtualMachine) -> PyResult { let zelf = zelf.0; - if zelf.zelf.is_none() || zelf.module.is_some() { + if zelf.zelf.is_none() || zelf.module.deref().is_some() { Ok(vm.ctx.new_str(zelf.value.name).into()) } else { let getattr = vm.builtins.get_attr("getattr", vm)?; diff --git a/crates/vm/src/builtins/classmethod.rs b/crates/vm/src/builtins/classmethod.rs index 46c0c7d76..cf459e573 100644 --- a/crates/vm/src/builtins/classmethod.rs +++ b/crates/vm/src/builtins/classmethod.rs @@ -184,15 +184,13 @@ impl Representable for PyClassMethod { let callable = zelf.callable.load_owned().repr(vm)?; let class = Self::class(&vm.ctx); + let module = class.__module__(vm)?; let repr = match ( class .__qualname__(vm) .downcast_ref::() .map(|n| n.as_wtf8()), - class - .__module__(vm) - .downcast_ref::() - .map(|m| m.as_wtf8()), + module.downcast_ref::().map(|m| m.as_wtf8()), ) { (None, _) => return Err(vm.new_type_error("Unknown qualified name")), (Some(qualname), Some(module)) if module != "builtins" => { diff --git a/crates/vm/src/builtins/code.rs b/crates/vm/src/builtins/code.rs index d2ea714d8..0a07cd49a 100644 --- a/crates/vm/src/builtins/code.rs +++ b/crates/vm/src/builtins/code.rs @@ -1,5 +1,6 @@ //! Infamous code object. The python class `code` +use super::descriptor::{MemberKind, MemberLayout}; use super::{PyBytesRef, PyStrRef, PyTupleRef, PyType, set::PyFrozenSet}; use crate::common::lock::PyMutex; #[cfg(feature = "host_env")] @@ -477,12 +478,16 @@ pub struct PyCode { #[pymember(name = "co_stacksize", path = "max_stackdepth")] #[pymember(name = "co_name", path = "obj_name")] #[pymember(name = "co_qualname", path = "qualname")] + #[pymember(name = "co_flags", path = "flags")] pub code: CodeObject, /// Slot-indexed names, equivalent to CPython's `co_localsplusnames`. /// Derived once so frame-local proxy operations do not repeatedly scan /// merged cell variables. localsplus_names: Box<[&'static PyStrInterned]>, + #[pymember(name = "co_filename")] source_path: AtomicPtr, + #[pymember(name = "co_nlocals")] + nlocals: i32, /// Version counter for lazy re-instrumentation. /// Compared against `PyGlobalState::instrumentation_version` at RESUME. pub instrumentation_version: AtomicU64, @@ -582,9 +587,14 @@ fn build_localspluskinds( Ok(kinds.into_boxed_slice()) } +impl MemberLayout for CodeFlags { + const KIND: MemberKind = MemberKind::Int; +} + impl PyCode { pub fn new(code: CodeObject) -> Self { let sp = code.source_path as *const PyStrInterned as *mut PyStrInterned; + let nlocals = i32::try_from(code.varnames.len()).unwrap_or(i32::MAX); let localsplus_names = { let varname_ids = code .varnames @@ -622,6 +632,7 @@ impl PyCode { code, localsplus_names, source_path: AtomicPtr::new(sp), + nlocals, instrumentation_version: AtomicU64::new(0), monitoring_data: PyMutex::new(None), quickened: core::sync::atomic::AtomicBool::new(false), @@ -1030,11 +1041,11 @@ impl Constructor for PyCode { } #[pyclass( + itemsize = core::mem::size_of::(), with(Representable, Constructor, Comparable, Hashable), flags(HAS_WEAKREF) )] impl PyCode { - #[pygetset] pub fn co_filename(&self) -> PyStrRef { self.source_path().to_owned() } @@ -1049,11 +1060,6 @@ impl PyCode { vm.ctx.new_tuple(cellvars) } - #[pygetset] - fn co_nlocals(&self) -> usize { - self.code.varnames.len() - } - #[pygetset] fn co_firstlineno(&self) -> u32 { self.code.first_line_number.map_or(0, |n| n.get() as _) @@ -1077,11 +1083,6 @@ impl PyCode { vm.ctx.new_tuple(names) } - #[pygetset] - const fn co_flags(&self) -> u32 { - self.code.flags.bits() - } - #[pygetset] pub fn co_varnames(&self, vm: &VirtualMachine) -> PyTupleRef { let varnames = self.code.varnames.iter().map(|s| s.to_object()).collect(); diff --git a/crates/vm/src/builtins/coroutine.rs b/crates/vm/src/builtins/coroutine.rs index 60ee5eac2..a199f2049 100644 --- a/crates/vm/src/builtins/coroutine.rs +++ b/crates/vm/src/builtins/coroutine.rs @@ -42,6 +42,7 @@ impl PyPayload for PyCoroutine { } #[pyclass( + itemsize = core::mem::size_of::(), flags(DISALLOW_INSTANTIATION, HAS_WEAKREF), with(Py, Representable, Destructor) )] diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index bb260cc60..193048a8a 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -17,6 +17,7 @@ use crate::{ SetattroFunc, StringifyFunc, }, }; +use core::mem::{align_of, size_of}; use rustpython_common::lock::PyRwLock; #[derive(Debug)] @@ -313,26 +314,64 @@ impl Representable for PyClassMethodDescriptor { #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(i32)] pub enum MemberKind { + /// `short`. Writable cells are `AtomicI16`. + Short = 0, Int = 1, + /// `long` (`c_long`). Writable cells are the atomic of that width. + Long = 2, Double = 4, + /// `char *`. The slot is a pointer to a NUL-terminated string. Null loads + /// as `None`. Assignment is rejected. + String = 5, Object = 6, + /// `unsigned short`. Writable cells are `AtomicU16`. + UShort = 10, Uint = 11, + /// `unsigned long` (`c_ulong`). Writable cells are the atomic of that width. + ULong = 12, Bool = 14, ObjectEx = 16, + /// `long long`. Writable cells are `AtomicI64`. + LongLong = 17, + /// `unsigned long long`. Writable cells are `AtomicU64`. + ULongLong = 18, /// `Py_ssize_t`. Writable cells are `AtomicIsize`. PySsizeT = 19, } impl MemberKind { + const fn from_i64() -> Self { + if size_of::() == 8 { + Self::Long + } else { + Self::LongLong + } + } + + const fn from_u64() -> Self { + if size_of::() == 8 { + Self::ULong + } else { + Self::ULongLong + } + } + #[must_use] pub fn from_i32(value: i32) -> Option { match value { + 0 => Some(Self::Short), 1 => Some(Self::Int), + 2 => Some(Self::Long), 4 => Some(Self::Double), + 5 => Some(Self::String), 6 => Some(Self::Object), + 10 => Some(Self::UShort), 11 => Some(Self::Uint), + 12 => Some(Self::ULong), 14 => Some(Self::Bool), 16 => Some(Self::ObjectEx), + 17 => Some(Self::LongLong), + 18 => Some(Self::ULongLong), 19 => Some(Self::PySsizeT), _ => None, } @@ -363,6 +402,75 @@ pub const fn member_kind_of( ::KIND } +impl MemberLayout for i16 { + const KIND: MemberKind = MemberKind::Short; +} +impl MemberLayout for core::sync::atomic::AtomicI16 { + const KIND: MemberKind = MemberKind::Short; +} +impl MemberCell for core::sync::atomic::AtomicI16 {} + +impl MemberLayout for u16 { + const KIND: MemberKind = MemberKind::UShort; +} +impl MemberLayout for core::sync::atomic::AtomicU16 { + const KIND: MemberKind = MemberKind::UShort; +} +impl MemberCell for core::sync::atomic::AtomicU16 {} + +// `i64` is `long` where `c_long` is 8 bytes, and `long long` otherwise. +impl MemberLayout for i64 { + const KIND: MemberKind = MemberKind::from_i64(); +} +impl MemberLayout for core::sync::atomic::AtomicI64 { + const KIND: MemberKind = MemberKind::from_i64(); +} +impl MemberCell for core::sync::atomic::AtomicI64 {} + +impl MemberLayout for u64 { + const KIND: MemberKind = MemberKind::from_u64(); +} +impl MemberLayout for core::sync::atomic::AtomicU64 { + const KIND: MemberKind = MemberKind::from_u64(); +} +impl MemberCell for core::sync::atomic::AtomicU64 {} + +/// Readonly `char *`. The word is the pointer, not the characters. +#[repr(transparent)] +#[derive(Copy, Clone)] +pub struct CStrMember(*const core::ffi::c_char); + +impl core::fmt::Debug for CStrMember { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("CStrMember").field("ptr", &self.0).finish() + } +} + +impl CStrMember { + #[must_use] + pub const fn new(ptr: *const core::ffi::c_char) -> Self { + Self(ptr) + } + + #[must_use] + pub const fn as_ptr(self) -> *const core::ffi::c_char { + self.0 + } +} + +// The pointer is not dereferenced by moving the wrapper. +unsafe impl Send for CStrMember {} +unsafe impl Sync for CStrMember {} + +const _: () = assert!( + size_of::() == size_of::<*const core::ffi::c_char>() + && align_of::() == align_of::<*const core::ffi::c_char>() +); + +impl MemberLayout for CStrMember { + const KIND: MemberKind = MemberKind::String; +} + impl MemberLayout for bool { const KIND: MemberKind = MemberKind::Bool; } @@ -405,6 +513,13 @@ impl MemberLayout for Option> { impl MemberLayout for crate::object::PyAtomicRef { const KIND: MemberKind = MemberKind::Object; } +// `PyAtomicRef` is the same pointer-sized cell as `PyAtomicRef` +// (`PhantomData` is zero-sized). Readonly object members may use it. The +// getter loads the slot as an object pointer. Writable cells stay +// `PyAtomicRef` or `PyAtomicRef>` (`MemberCell`). +impl MemberLayout for crate::object::PyAtomicRef { + const KIND: MemberKind = MemberKind::Object; +} impl MemberLayout for crate::object::PyAtomicRef> { const KIND: MemberKind = MemberKind::Object; } @@ -436,6 +551,34 @@ impl MemberLayout for core::sync::atomic::AtomicIsize { } impl MemberCell for core::sync::atomic::AtomicIsize {} +// `usize` and `isize` are the same width. A readonly `Py_ssize_t` member may +// be a `usize` that is not written after publication and whose value fits in +// `isize` (the getter reads the bits as `isize`). +const _: () = + assert!(size_of::() == size_of::() && align_of::() == align_of::()); +impl MemberLayout for usize { + const KIND: MemberKind = MemberKind::PySsizeT; +} + +// Readonly `co_filename`. The field is `AtomicPtr`: the pointer +// is never null, and `PyStrInterned` is `repr(transparent)` over `Py`, +// so the word is an object pointer. `member_get_one` reads an Object member +// through `PyAtomicRef>` (`get_slot`). That cell is +// `AtomicPtr` when `threading` is on and `Cell<*mut u8>` otherwise; both +// are one pointer, and `AtomicPtr` is one pointer in either +// build (`Atomic<*mut T>` stores `Align8<*mut T>`, same size and alignment as +// the pointer). The load copies that word and increfs. Interned strings live +// for the process, and the slot does not own the reference. +const _: () = assert!( + size_of::>() + == size_of::>>() + && align_of::>() + == align_of::>>() +); +impl MemberLayout for core::sync::atomic::AtomicPtr { + const KIND: MemberKind = MemberKind::Object; +} + /// Where `PyMemberDef.offset` points. /// /// `Offset` is a byte offset from the object to the field. @@ -447,6 +590,18 @@ pub enum MemberAccess { TupleItem, } +/// C layout of `PyMemberDef`: name pointer, int type, `Py_ssize_t` offset, +/// int flags, doc pointer. `size_of` of this is `type`'s `tp_itemsize`. +#[repr(C)] +#[allow(dead_code)] +pub struct PyMemberDefLayout { + _name: *const core::ffi::c_char, + _type: core::ffi::c_int, + _offset: isize, + _flags: core::ffi::c_int, + _doc: *const core::ffi::c_char, +} + /// Same fields as `PyMemberDef`: name, type, offset, flags, doc. pub struct PyMemberDef { pub name: String, @@ -539,7 +694,14 @@ impl PyMemberDescriptor { MemberKind::Bool | MemberKind::Double | MemberKind::Int + | MemberKind::Long + | MemberKind::LongLong + | MemberKind::Short + | MemberKind::String | MemberKind::Uint + | MemberKind::ULong + | MemberKind::ULongLong + | MemberKind::UShort | MemberKind::PySsizeT => None, } } @@ -689,6 +851,168 @@ fn load_i32(obj: &PyObject, offset: isize, readonly: bool) -> i32 { } } +fn load_i16(obj: &PyObject, offset: isize, readonly: bool) -> i16 { + let addr = member_addr(obj, offset); + if readonly { + // SAFETY: a readonly short member addresses an `i16` that is not written + // after publication. + unsafe { addr.cast::().read() } + } else { + // SAFETY: a writable short member addresses an aligned `AtomicI16`. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + } +} + +fn load_u16(obj: &PyObject, offset: isize, readonly: bool) -> u16 { + let addr = member_addr(obj, offset); + if readonly { + // SAFETY: a readonly unsigned short member addresses a `u16` that is not + // written after publication. + unsafe { addr.cast::().read() } + } else { + // SAFETY: a writable unsigned short member addresses an aligned `AtomicU16`. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + } +} + +fn load_i64(obj: &PyObject, offset: isize, readonly: bool) -> i64 { + let addr = member_addr(obj, offset); + if readonly { + // SAFETY: a readonly `long long` member addresses an `i64` that is not + // written after publication. + unsafe { addr.cast::().read() } + } else { + // SAFETY: a writable `long long` member addresses an aligned `AtomicI64`. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + } +} + +fn load_u64(obj: &PyObject, offset: isize, readonly: bool) -> u64 { + let addr = member_addr(obj, offset); + if readonly { + // SAFETY: a readonly `unsigned long long` member is a `u64` or an + // `AtomicU64` of the same size and alignment. A relaxed atomic load + // reads either; a plain integer is not written after publication, and + // an atomic word (type flags) is updated in place. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + } else { + // SAFETY: a writable `unsigned long long` member addresses an aligned `AtomicU64`. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) + } + } +} + +#[allow(clippy::unnecessary_cast)] // `c_long` is `i32` or `i64` +fn load_c_long(obj: &PyObject, offset: isize, readonly: bool) -> core::ffi::c_long { + let addr = member_addr(obj, offset); + if readonly { + // SAFETY: a readonly `long` member addresses a `c_long` that is not + // written after publication. + unsafe { addr.cast::().read() } + } else if size_of::() == 8 { + // SAFETY: a writable `long` member addresses an aligned `AtomicI64`. + // `c_long` is 8 bytes on this target. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) as core::ffi::c_long + } + } else { + // SAFETY: a writable `long` member addresses an aligned `AtomicI32`. + // `c_long` is 4 bytes on this target. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) as core::ffi::c_long + } + } +} + +#[allow(clippy::unnecessary_cast)] // `c_long` is `i32` or `i64` +fn store_c_long(obj: &PyObject, offset: isize, value: core::ffi::c_long) { + let addr = member_addr(obj, offset); + if size_of::() == 8 { + // SAFETY: a writable `long` member addresses an aligned `AtomicI64`. + // `c_long` is 8 bytes on this target. + unsafe { + (*addr.cast::()) + .store(value as i64, core::sync::atomic::Ordering::Relaxed); + } + } else { + // SAFETY: a writable `long` member addresses an aligned `AtomicI32`. + // `c_long` is 4 bytes on this target. + unsafe { + (*addr.cast::()) + .store(value as i32, core::sync::atomic::Ordering::Relaxed); + } + } +} + +#[allow(clippy::unnecessary_cast)] // `c_ulong` is `u32` or `u64` +fn load_c_ulong(obj: &PyObject, offset: isize, readonly: bool) -> core::ffi::c_ulong { + let addr = member_addr(obj, offset); + if readonly && size_of::() == 8 { + // SAFETY: a readonly `unsigned long` member is a `c_ulong` or an + // `AtomicU64` of the same size and alignment. A relaxed atomic load + // reads either; a plain integer is not written after publication, and + // an atomic word (type flags) is updated in place. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) as core::ffi::c_ulong + } + } else if readonly { + // SAFETY: a readonly `unsigned long` member addresses a `c_ulong` that + // is not written after publication. `c_ulong` is 4 bytes on this target. + unsafe { addr.cast::().read() } + } else if size_of::() == 8 { + // SAFETY: a writable `unsigned long` member addresses an aligned `AtomicU64`. + // `c_ulong` is 8 bytes on this target. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) as core::ffi::c_ulong + } + } else { + // SAFETY: a writable `unsigned long` member addresses an aligned `AtomicU32`. + // `c_ulong` is 4 bytes on this target. + unsafe { + (*addr.cast::()) + .load(core::sync::atomic::Ordering::Relaxed) as core::ffi::c_ulong + } + } +} + +#[allow(clippy::unnecessary_cast)] // `c_ulong` is `u32` or `u64` +fn store_c_ulong(obj: &PyObject, offset: isize, value: core::ffi::c_ulong) { + let addr = member_addr(obj, offset); + if size_of::() == 8 { + // SAFETY: a writable `unsigned long` member addresses an aligned `AtomicU64`. + // `c_ulong` is 8 bytes on this target. + unsafe { + (*addr.cast::()) + .store(value as u64, core::sync::atomic::Ordering::Relaxed); + } + } else { + // SAFETY: a writable `unsigned long` member addresses an aligned `AtomicU32`. + // `c_ulong` is 4 bytes on this target. + unsafe { + (*addr.cast::()) + .store(value as u32, core::sync::atomic::Ordering::Relaxed); + } + } +} + fn load_u32(obj: &PyObject, offset: isize, readonly: bool) -> u32 { let addr = member_addr(obj, offset); if readonly { @@ -740,13 +1064,16 @@ fn member_get_one( ) -> PyResult { let value = match member.kind { MemberKind::Object => obj.get_slot(offset).unwrap_or_else(|| vm.ctx.none()), - MemberKind::ObjectEx => obj.get_slot(offset).ok_or_else(|| { - vm.new_attribute_error(format!( - "'{}' object has no attribute '{}'", - obj.class().fully_qualified_name(vm), - member.name - )) - })?, + MemberKind::ObjectEx => match obj.get_slot(offset) { + Some(value) => value, + None => { + return Err(vm.new_attribute_error(format!( + "'{}' object has no attribute '{}'", + obj.class().fully_qualified_name(vm)?, + member.name + ))); + } + }, MemberKind::Bool => { // SAFETY: a bool member addresses an `AtomicBool` or a `bool` // (one byte, naturally aligned). The macro rejects any other field @@ -797,6 +1124,56 @@ fn member_get_one( }; vm.ctx.new_int(raw).into() } + MemberKind::Short => vm + .ctx + .new_int(load_i16(obj, offset, member.readonly())) + .into(), + MemberKind::UShort => vm + .ctx + .new_int(load_u16(obj, offset, member.readonly())) + .into(), + MemberKind::Long => vm + .ctx + .new_int(load_c_long(obj, offset, member.readonly())) + .into(), + MemberKind::LongLong => vm + .ctx + .new_int(load_i64(obj, offset, member.readonly())) + .into(), + MemberKind::ULong => vm + .ctx + .new_int(load_c_ulong(obj, offset, member.readonly())) + .into(), + MemberKind::ULongLong => vm + .ctx + .new_int(load_u64(obj, offset, member.readonly())) + .into(), + MemberKind::String => { + // SAFETY: a string member addresses a `*const c_char` that is not + // written after publication. Null is `None`. + let ptr = unsafe { + member_addr(obj, offset) + .cast::<*const core::ffi::c_char>() + .read() + }; + if ptr.is_null() { + vm.ctx.none() + } else { + // SAFETY: `ptr` is non-null. The slot points at a `CString` the + // owner keeps alive, so the bytes stay NUL-terminated for this read. + let bytes = unsafe { core::ffi::CStr::from_ptr(ptr) }.to_bytes(); + let Ok(text) = core::str::from_utf8(bytes) else { + return Err(vm.new_unicode_decode_error( + vm.ctx.new_str("utf-8"), + vm.ctx.new_bytes(bytes.to_vec()), + 0, + bytes.len(), + vm.ctx.new_str("invalid UTF-8"), + )); + }; + vm.ctx.new_str(text).into() + } + } }; Ok(value) } @@ -908,10 +1285,131 @@ fn member_set_one( .store(stored, core::sync::atomic::Ordering::Relaxed); } } + MemberKind::Short => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let long_val = member_as_c_long(&value, vm)?; + let stored = long_val as i16; + // SAFETY: a writable short member addresses an aligned `AtomicI16`. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + if long_val > i16::MAX as core::ffi::c_long || long_val < i16::MIN as core::ffi::c_long + { + warn_member(vm, "Truncation of value to short")?; + } + } + MemberKind::UShort => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let long_val = member_as_c_long(&value, vm)?; + let stored = long_val as u16; + // SAFETY: a writable unsigned short member addresses an aligned `AtomicU16`. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + if long_val > u16::MAX as core::ffi::c_long || long_val < 0 { + warn_member(vm, "Truncation of value to unsigned short")?; + } + } + MemberKind::Long => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let stored = member_as_c_long(&value, vm)?; + store_c_long(obj, offset, stored); + } + MemberKind::LongLong => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let stored = member_as_c_longlong(&value, vm)?; + // SAFETY: a writable `long long` member addresses an aligned `AtomicI64`. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + } + MemberKind::ULong => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let (stored, warning) = member_ulong_value(&value, vm)?; + store_c_ulong(obj, offset, stored); + if let Some(warning) = warning { + warn_member(vm, warning)?; + } + } + MemberKind::ULongLong => { + let PySetterValue::Assign(value) = value else { + return Err(vm.new_type_error("can't delete numeric/char attribute")); + }; + let (stored, warning) = member_ulonglong_value(&value, vm)?; + // SAFETY: a writable `unsigned long long` member addresses an aligned `AtomicU64`. + unsafe { + (*member_addr(obj, offset).cast::()) + .store(stored, core::sync::atomic::Ordering::Relaxed); + } + if let Some(warning) = warning { + warn_member(vm, warning)?; + } + } + MemberKind::String => { + return Err(vm.new_type_error("readonly attribute")); + } } Ok(()) } +fn member_as_c_longlong(value: &PyObject, vm: &VirtualMachine) -> PyResult { + let int_obj = value.try_index(vm)?; + i64::try_from(int_obj.as_bigint()) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long long")) +} + +fn member_ulong_value( + value: &PyObject, + vm: &VirtualMachine, +) -> PyResult<(core::ffi::c_ulong, Option<&'static str>)> { + let int_obj = value.try_index(vm)?; + let big = int_obj.as_bigint(); + if big.sign() == malachite_bigint::Sign::Minus { + let long_val = core::ffi::c_long::try_from(big) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long"))?; + return Ok(( + long_val as core::ffi::c_ulong, + Some("Writing negative value into unsigned field"), + )); + } + let stored = core::ffi::c_ulong::try_from(big) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C unsigned long"))?; + Ok((stored, None)) +} + +fn member_ulonglong_value( + value: &PyObject, + vm: &VirtualMachine, +) -> PyResult<(u64, Option<&'static str>)> { + let int_obj = value.try_index(vm)?; + let big = int_obj.as_bigint(); + if big.sign() == malachite_bigint::Sign::Minus { + let long_val = core::ffi::c_long::try_from(big) + .map_err(|_| vm.new_overflow_error("Python int too large to convert to C long"))?; + return Ok(( + long_val as u64, + Some("Writing negative value into unsigned field"), + )); + } + let stored = u64::try_from(big).map_err(|_| { + vm.new_overflow_error("Python int too large to convert to C unsigned long long") + })?; + Ok((stored, None)) +} + impl Representable for PyMemberDescriptor { #[inline] fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { diff --git a/crates/vm/src/builtins/frame.rs b/crates/vm/src/builtins/frame.rs index ceb0779a4..bbb97da4c 100644 --- a/crates/vm/src/builtins/frame.rs +++ b/crates/vm/src/builtins/frame.rs @@ -465,7 +465,11 @@ impl FrameObject { } } -#[pyclass(flags(DISALLOW_INSTANTIATION), with(Py, Representable))] +#[pyclass( + itemsize = core::mem::size_of::(), + flags(DISALLOW_INSTANTIATION), + with(Py, Representable) +)] impl FrameObject { #[pygetset] pub fn f_globals(&self) -> PyDictRef { diff --git a/crates/vm/src/builtins/generator.rs b/crates/vm/src/builtins/generator.rs index 2c14a0bfc..201b39060 100644 --- a/crates/vm/src/builtins/generator.rs +++ b/crates/vm/src/builtins/generator.rs @@ -39,6 +39,7 @@ impl PyPayload for PyGenerator { } #[pyclass( + itemsize = core::mem::size_of::(), flags(DISALLOW_INSTANTIATION, HAS_WEAKREF), with(Py, IterNext, Iterable, Representable, Destructor) )] diff --git a/crates/vm/src/builtins/genericalias.rs b/crates/vm/src/builtins/genericalias.rs index 46de0bec6..59ec0e430 100644 --- a/crates/vm/src/builtins/genericalias.rs +++ b/crates/vm/src/builtins/genericalias.rs @@ -562,11 +562,11 @@ pub(crate) fn subs_parameters( }; if unpack { + let arg_type = arg.class().fully_qualified_name(vm)?; + let substituted_type = substituted_arg.class().fully_qualified_name(vm)?; let tuple = substituted_arg.try_to_ref::(vm).map_err(|_| { vm.new_type_error(format!( - "expected __typing_subst__ of {} objects to return a tuple, not {}", - arg.class().fully_qualified_name(vm), - substituted_arg.class().fully_qualified_name(vm), + "expected __typing_subst__ of {arg_type} objects to return a tuple, not {substituted_type}", )) })?; for elem in tuple { diff --git a/crates/vm/src/builtins/memory.rs b/crates/vm/src/builtins/memory.rs index 3ceb297dd..247fcc32d 100644 --- a/crates/vm/src/builtins/memory.rs +++ b/crates/vm/src/builtins/memory.rs @@ -675,6 +675,7 @@ impl Py { } #[pyclass( + itemsize = core::mem::size_of::(), with( Py, Hashable, diff --git a/crates/vm/src/builtins/namespace.rs b/crates/vm/src/builtins/namespace.rs index f47caf818..a9ae6a9af 100644 --- a/crates/vm/src/builtins/namespace.rs +++ b/crates/vm/src/builtins/namespace.rs @@ -12,6 +12,9 @@ use crate::{ use rustpython_common::wtf8::Wtf8Buf; #[pyclass(module = "types", name = "SimpleNamespace")] +// The dict lives in the object extension, not the payload. The offset is +// the dict cell at the front of that extension. +#[pymember(name = "__dict__", offset = ::rustpython_vm::object::dict_member_offset())] #[derive(Debug, Default)] pub struct PyNamespace {} diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index dc76d29c1..1f59f1290 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -212,8 +212,8 @@ fn object_getstate_default(obj: &PyObject, required: bool, vm: &VirtualMachine) if required { // Dict, weakref list, and slot cells sit in the prefix in front of - // the payload, so they are not part of `slots.basicsize`. Only state - // stored inside the payload counts. + // the object header. `slots.basicsize` is the header plus the payload, + // so only state stored inside the payload counts. let basicsize = vm.ctx.types.object_type.slots().basicsize; if obj.class().slots().basicsize > basicsize { return Err(vm.new_type_error(format!("cannot pickle '{}' object", obj.class().name()))); @@ -343,14 +343,16 @@ impl PyBaseObject { #[pyslot] fn slot_repr(zelf: &PyObject, vm: &VirtualMachine) -> PyResult { let class = zelf.class(); + // A missing `__module__` is not an error here. + let module = class.__module__(vm).ok(); match ( class .__qualname__(vm) .downcast_ref::() .map(|n| n.as_wtf8()), - class - .__module__(vm) - .downcast_ref::() + module + .as_ref() + .and_then(|m| m.downcast_ref::()) .map(|m| m.as_wtf8()), ) { (None, _) => Err(vm.new_type_error("Unknown qualified name")), @@ -506,7 +508,11 @@ impl PyBaseObject { #[pymethod] fn __sizeof__(zelf: PyObjectRef) -> usize { - zelf.class().slots().basicsize + // `slots.basicsize` includes the object header. This reports the payload. + zelf.class() + .slots() + .basicsize + .saturating_sub(crate::object::SIZEOF_PYOBJECT_HEAD) } } diff --git a/crates/vm/src/builtins/staticmethod.rs b/crates/vm/src/builtins/staticmethod.rs index 8198c35c4..6bdf7d81b 100644 --- a/crates/vm/src/builtins/staticmethod.rs +++ b/crates/vm/src/builtins/staticmethod.rs @@ -198,15 +198,13 @@ impl Representable for PyStaticMethod { let callable = zelf.callable.load_owned().repr(vm)?; let class = Self::class(&vm.ctx); + let module = class.__module__(vm)?; match ( class .__qualname__(vm) .downcast_ref::() .map(|n| n.as_wtf8()), - class - .__module__(vm) - .downcast_ref::() - .map(|m| m.as_wtf8()), + module.downcast_ref::().map(|m| m.as_wtf8()), ) { (None, _) => Err(vm.new_type_error("Unknown qualified name")), (Some(qualname), Some(module)) if module != "builtins" => { diff --git a/crates/vm/src/builtins/super.rs b/crates/vm/src/builtins/super.rs index 3ad41bdb1..9eb76e9df 100644 --- a/crates/vm/src/builtins/super.rs +++ b/crates/vm/src/builtins/super.rs @@ -9,32 +9,52 @@ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, builtins::function::PyCell, class::PyClassImpl, - common::lock::PyRwLock, function::{FuncArgs, IntoFuncArgs, OptionalArg}, + object::PyAtomicRef, types::{Callable, Constructor, GetAttr, GetDescriptor, Initializer, Representable}, }; #[pyclass(module = false, name = "super", traverse)] #[derive(Debug)] pub struct PySuper { - inner: PyRwLock, + #[pymember(name = "__thisclass__")] + typ: PyAtomicRef>, + #[pymember(name = "__self__")] + obj: PyAtomicRef>, + #[pymember(name = "__self_class__")] + obj_type: PyAtomicRef>, } -#[derive(Debug, Traverse)] -struct PySuperInner { +fn bind_super( typ: PyTypeRef, - obj: Option<(PyObjectRef, PyTypeRef)>, + obj: PyObjectRef, + vm: &VirtualMachine, +) -> PyResult<(PyTypeRef, Option, Option)> { + if vm.is_none(&obj) { + return Ok((typ, None, None)); + } + let obj_type = super_check(&typ, &obj, vm)?; + Ok((typ, Some(obj), Some(obj_type))) } -impl PySuperInner { - fn new(typ: PyTypeRef, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { - let obj = if vm.is_none(&obj) { - None - } else { - let obj_type = super_check(&typ, &obj, vm)?; - Some((obj, obj_type)) - }; - Ok(Self { typ, obj }) +impl PySuper { + fn empty() -> Self { + Self { + typ: PyAtomicRef::from(None), + obj: PyAtomicRef::from(None), + obj_type: PyAtomicRef::from(None), + } + } + + fn store_bound( + &self, + typ: Option, + obj: Option, + obj_type: Option, + ) { + drop(self.typ.store(typ)); + drop(self.obj.store(obj)); + drop(self.obj_type.store(obj_type)); } } @@ -48,14 +68,9 @@ impl PyPayload for PySuper { impl Constructor for PySuper { type Args = FuncArgs; - fn py_new(_cls: &Py, _args: Self::Args, vm: &VirtualMachine) -> PyResult { - Ok(Self { - inner: PyRwLock::new(PySuperInner::new( - vm.ctx.types.object_type.to_owned(), // is this correct? - vm.ctx.none(), - vm, - )?), - }) + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + // `tp_new` leaves every field null. `tp_init` fills them in. + Ok(Self::empty()) } } @@ -80,106 +95,109 @@ impl Initializer for PySuper { Self::Args { py_type, object }: Self::Args, vm: &VirtualMachine, ) -> PyResult<()> { - // Get the type: - let (typ, obj) = if let OptionalArg::Present(ty) = py_type { - (ty, object.unwrap_or_none(vm)) - } else { - // Access the InterpreterFrame directly — no need to materialize - // a FrameObject just to read code/locals. - let iframe_ptr = crate::vm::thread::get_current_frame(); - if iframe_ptr.is_null() { - return Err(vm.new_runtime_error("super(): no current frame")); - } - let iframe = unsafe { &*iframe_ptr }; - let code = iframe.code(); - - if code.arg_count == 0 { - return Err(vm.new_runtime_error("super(): no arguments")); - } - - // SAFETY: InterpreterFrame is current and not concurrently mutated. - use rustpython_compiler_core::bytecode::CO_FAST_CELL; - let fastlocals = iframe.localsplus.fastlocals(); - let obj = fastlocals[0] - .clone() - .and_then(|val| { - // If slot 0 is a merged cell (LOCAL|CELL), extract value from cell - if code - .localspluskinds - .first() - .is_some_and(|&k| k & CO_FAST_CELL != 0) - { - val.downcast_ref::().and_then(|c| c.get()) - } else { - Some(val) - } - }) - .ok_or_else(|| vm.new_runtime_error("super(): arg[0] deleted"))?; - - let mut typ = None; - // Search for __class__ in freevars using localspluskinds - let nlocalsplus = code.localspluskinds.len(); - let nfrees = code.freevars.len(); - let free_start = nlocalsplus - nfrees; - for (i, var) in code.freevars.iter().enumerate() { - if var.as_bytes() == b"__class__" { - let class = fastlocals[free_start + i] - .as_ref() - .and_then(|v| v.downcast_ref::()) - .and_then(|c| c.get()) - .ok_or_else(|| vm.new_runtime_error("super(): empty __class__ cell"))?; - typ = Some(class.downcast().map_err(|o| { - vm.new_type_error(format!( - "super(): __class__ is not a type ({})", - o.class().name() - )) - })?); - break; - } - } - let typ = typ.ok_or_else(|| { - vm.new_type_error( - "super must be called with 1 argument or from inside class method", - ) - })?; - - (typ, obj) + let typ = match py_type { + OptionalArg::Present(ty) => Some(ty), + OptionalArg::Missing => None, }; - - let inner = PySuperInner::new(typ, obj, vm)?; - *zelf.inner.write() = inner; - - Ok(()) + let obj = match object { + OptionalArg::Present(obj) => Some(obj), + OptionalArg::Missing => None, + }; + super_init_impl(zelf, typ, obj, vm) } } +/// `super_init_impl`. A null type takes the zero-argument path. +fn super_init_impl( + zelf: &Py, + typ: Option, + obj: Option, + vm: &VirtualMachine, +) -> PyResult<()> { + let (typ, obj) = match typ { + Some(typ) => (typ, obj.unwrap_or_else(|| vm.ctx.none())), + None => super_init_without_args(vm)?, + }; + let (typ, obj, obj_type) = bind_super(typ, obj, vm)?; + zelf.store_bound(Some(typ), obj, obj_type); + Ok(()) +} + +fn super_init_without_args(vm: &VirtualMachine) -> PyResult<(PyTypeRef, PyObjectRef)> { + // Access the InterpreterFrame directly — no need to materialize + // a FrameObject just to read code/locals. + let iframe_ptr = crate::vm::thread::get_current_frame(); + if iframe_ptr.is_null() { + return Err(vm.new_runtime_error("super(): no current frame")); + } + let iframe = unsafe { &*iframe_ptr }; + let code = iframe.code(); + + if code.arg_count == 0 { + return Err(vm.new_runtime_error("super(): no arguments")); + } + + // SAFETY: InterpreterFrame is current and not concurrently mutated. + use rustpython_compiler_core::bytecode::CO_FAST_CELL; + let fastlocals = iframe.localsplus.fastlocals(); + let obj = fastlocals[0] + .clone() + .and_then(|val| { + // If slot 0 is a merged cell (LOCAL|CELL), extract value from cell + if code + .localspluskinds + .first() + .is_some_and(|&k| k & CO_FAST_CELL != 0) + { + val.downcast_ref::().and_then(|c| c.get()) + } else { + Some(val) + } + }) + .ok_or_else(|| vm.new_runtime_error("super(): arg[0] deleted"))?; + + let mut typ = None; + // Search for __class__ in freevars using localspluskinds + let nlocalsplus = code.localspluskinds.len(); + let nfrees = code.freevars.len(); + let free_start = nlocalsplus - nfrees; + for (i, var) in code.freevars.iter().enumerate() { + if var.as_bytes() == b"__class__" { + let class = fastlocals[free_start + i] + .as_ref() + .and_then(|v| v.downcast_ref::()) + .and_then(|c| c.get()) + .ok_or_else(|| vm.new_runtime_error("super(): empty __class__ cell"))?; + typ = Some(class.downcast().map_err(|o| { + vm.new_type_error(format!( + "super(): __class__ is not a type ({})", + o.class().name() + )) + })?); + break; + } + } + let typ = typ.ok_or_else(|| { + vm.new_type_error("super must be called with 1 argument or from inside class method") + })?; + + Ok((typ, obj)) +} + #[pyclass( with(GetAttr, GetDescriptor, Constructor, Initializer, Representable), flags(BASETYPE) )] -impl PySuper { - #[pygetset] - fn __thisclass__(&self) -> PyTypeRef { - self.inner.read().typ.clone() - } - - #[pygetset] - fn __self_class__(&self) -> Option { - Some(self.inner.read().obj.as_ref()?.1.clone()) - } - - #[pygetset] - fn __self__(&self) -> Option { - Some(self.inner.read().obj.as_ref()?.0.clone()) - } -} +impl PySuper {} impl GetAttr for PySuper { fn getattro(zelf: &Py, name: &Py, vm: &VirtualMachine) -> PyResult { let skip = |zelf: &Py, name| zelf.as_object().generic_getattr(name, vm); - let (obj, start_type): (PyObjectRef, PyTypeRef) = match &zelf.inner.read().obj { - Some(o) => o.clone(), - None => return skip(zelf, name), + let Some(obj) = zelf.obj.load_owned() else { + return skip(zelf, name); + }; + let Some(start_type) = zelf.obj_type.load_owned() else { + return skip(zelf, name); }; // We want __class__ to return the class of the super object @@ -195,7 +213,9 @@ impl GetAttr for PySuper { // Both locks are dropped before any arbitrary Python code // (the descriptor call below) runs, so they can't be held // across a call that might re-enter and want them again. - let su_type = zelf.inner.read().typ.clone(); + let Some(su_type) = zelf.typ.load_owned() else { + return skip(zelf, name); + }; let descr = { let mro = start_type.mro.read(); mro.iter() @@ -230,20 +250,23 @@ impl GetDescriptor for PySuper { vm: &VirtualMachine, ) -> PyResult { let (zelf, obj) = Self::_unwrap(zelf_obj, obj, vm)?; - if vm.is_none(obj) || zelf.inner.read().obj.is_some() { + if vm.is_none(obj) || zelf.obj.deref().is_some() { return Ok(zelf_obj.to_owned()); } let zelf_class = zelf.as_object().class(); + let typ = zelf.typ.load_owned(); if zelf_class.is(vm.ctx.types.super_type) { - let typ = zelf.inner.read().typ.clone(); - Ok(Self { - inner: PyRwLock::new(PySuperInner::new(typ, obj.to_owned(), vm)?), - } - .into_ref(&vm.ctx) - .into()) + let newobj = Self::empty().into_ref(&vm.ctx); + // A null type takes the zero-argument path inside super_init_impl. + super_init_impl(&newobj, typ, Some(obj.to_owned()), vm)?; + Ok(newobj.into()) } else { - let typ = zelf.inner.read().typ.clone(); - PyType::call(zelf.class(), (typ, obj.to_owned()).into_args(vm), vm) + // Call stops at the first null argument. + let args = match typ { + Some(typ) => (typ, obj.to_owned()).into_args(vm), + None => FuncArgs::default(), + }; + PyType::call(zelf.class(), args, vm) } } } @@ -251,12 +274,12 @@ impl GetDescriptor for PySuper { impl Representable for PySuper { #[inline] fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - let type_name = zelf.inner.read().typ.name().to_owned(); - let obj = zelf.inner.read().obj.clone(); - let repr = match obj { - Some((_, ref ty)) => { - format!(", <{} object>>", type_name, ty.name()) - } + let type_name = match zelf.typ.load_owned() { + Some(ty) => ty.name().to_owned(), + None => "NULL".to_owned(), + }; + let repr = match zelf.obj_type.load_owned() { + Some(ty) => format!(", <{} object>>", type_name, ty.name()), None => format!(", NULL>"), }; Ok(repr) diff --git a/crates/vm/src/builtins/traceback.rs b/crates/vm/src/builtins/traceback.rs index 0e93768dc..7f8a2dde2 100644 --- a/crates/vm/src/builtins/traceback.rs +++ b/crates/vm/src/builtins/traceback.rs @@ -10,9 +10,11 @@ use rustpython_compiler_core::OneIndexed; #[derive(Debug)] pub struct PyTraceback { pub next: PyMutex>, + #[pymember(name = "tb_frame", audit_read)] pub frame: FrameObjectRef, + #[pymember(name = "tb_lasti")] #[pytraverse(skip)] - pub lasti: u32, + pub lasti: i32, #[pytraverse(skip)] pub lineno: OneIndexed, } @@ -32,7 +34,7 @@ impl PyTraceback { pub const fn new( next: Option>, frame: FrameObjectRef, - lasti: u32, + lasti: i32, lineno: OneIndexed, ) -> Self { Self { @@ -43,16 +45,6 @@ impl PyTraceback { } } - #[pygetset] - fn tb_frame(&self) -> FrameObjectRef { - self.frame.clone() - } - - #[pygetset] - const fn tb_lasti(&self) -> u32 { - self.lasti - } - #[pygetset] const fn tb_lineno(&self) -> usize { self.lineno.get() @@ -104,7 +96,7 @@ impl PyTraceback { } impl Constructor for PyTraceback { - type Args = (Option>, FrameObjectRef, u32, usize); + type Args = (Option>, FrameObjectRef, i32, usize); fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { let (next, frame, lasti, lineno) = args; diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 4330e7110..91f10b267 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -15,7 +15,6 @@ use crate::{ }, class::{PyClassDef, PyClassImpl, StaticType}, common::{ - ascii, borrow::BorrowedValue, lock::{PyRwLock, PyRwLockReadGuard}, }, @@ -37,7 +36,7 @@ use core::{ cell::Cell, ops::Deref, pin::Pin, - sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering}, + sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering}, }; use indexmap::{IndexMap, map::Entry}; use itertools::Itertools; @@ -56,11 +55,13 @@ pub struct PyType { pub mro: PyRwLock>, pub subclasses: PyRwLock>>, pub attributes: TypeNamespace, + #[pymember(name = "__itemsize__", path = "itemsize")] + #[pymember(name = "__basicsize__", path = "basicsize")] + #[pymember(name = "__flags__", path = "flags")] pub slots: PyTypeSlots, pub heaptype_ext: Option>>, /// Type version tag for inline caching. 0 means unassigned/invalidated. pub tp_version_tag: AtomicU32, - pub abc_tpflags: AtomicU64, } /// Monotonic counter for type version tags. Once it reaches `u32::MAX`, @@ -851,9 +852,7 @@ impl PyType { // Check each base in order and inherit the first collection flag found for base in bases { - let base_flags = (base.slots.flags - | PyTypeFlags::from_bits_truncate(base.abc_tpflags.load(Ordering::Acquire))) - & COLLECTION_FLAGS; + let base_flags = base.slots.flags.load() & COLLECTION_FLAGS; if !base_flags.is_empty() { slots.flags |= base_flags; return; @@ -861,40 +860,21 @@ impl PyType { } } - fn inherited_abc_tpflags(bases: &[PyRef]) -> u64 { - const COLLECTION_FLAGS: PyTypeFlags = PyTypeFlags::from_bits_truncate( - PyTypeFlags::SEQUENCE.bits() | PyTypeFlags::MAPPING.bits(), - ); - for base in bases { - let base_flags = - PyTypeFlags::from_bits_truncate(base.abc_tpflags.load(Ordering::Acquire)) - & COLLECTION_FLAGS; - if !base_flags.is_empty() { - return base_flags.bits(); - } - } - 0 - } - pub fn has_patma_collection_flag(&self, flag: PyTypeFlags) -> bool { debug_assert!(matches!(flag, PyTypeFlags::SEQUENCE | PyTypeFlags::MAPPING)); const COLLECTION_FLAGS: PyTypeFlags = PyTypeFlags::from_bits_truncate( PyTypeFlags::SEQUENCE.bits() | PyTypeFlags::MAPPING.bits(), ); - let slot_flags = self.slots.flags & COLLECTION_FLAGS; - if !slot_flags.is_empty() { - return slot_flags.contains(flag); - } - PyTypeFlags::from_bits_truncate(self.abc_tpflags.load(Ordering::Acquire)).contains(flag) + let slot_flags = self.slots.flags.load() & COLLECTION_FLAGS; + slot_flags.contains(flag) } pub fn set_is_abstract(&self, is_abstract: bool) { - const MASK: u64 = PyTypeFlags::IS_ABSTRACT.bits(); - let _ = self - .abc_tpflags - .try_update(Ordering::AcqRel, Ordering::Acquire, |old| { - Some(if is_abstract { old | MASK } else { old & !MASK }) - }); + if is_abstract { + self.slots.flags.set(PyTypeFlags::IS_ABSTRACT); + } else { + self.slots.flags.remove(PyTypeFlags::IS_ABSTRACT); + } self.modified(); } @@ -906,13 +886,7 @@ impl PyType { if flags.is_empty() { return; } - let collection_bits = COLLECTION_FLAGS.bits(); - let flags_bits = flags.bits(); - let _ = self - .abc_tpflags - .try_update(Ordering::AcqRel, Ordering::Acquire, |old| { - Some((old & !collection_bits) | flags_bits) - }); + self.slots.flags.replace_masked(COLLECTION_FLAGS, flags); self.modified(); for weak_ref in self.subclasses.read().iter() { if let Some(subclass) = weak_ref.upgrade() @@ -1052,7 +1026,6 @@ impl PyType { )); } - let inherited_abc_tpflags = Self::inherited_abc_tpflags(bases.as_slice()); let new_type = PyRef::new_ref( Self { base: Some(base).into(), @@ -1063,7 +1036,6 @@ impl PyType { slots, heaptype_ext: Some(Pin::new(Box::new(heaptype_ext))), tp_version_tag: AtomicU32::new(0), - abc_tpflags: AtomicU64::new(inherited_abc_tpflags), }, metaclass, None, @@ -1112,7 +1084,6 @@ impl PyType { slots.flags |= PyTypeFlags::MANAGED_WEAKREF; } - let inherited_abc_tpflags = Self::inherited_abc_tpflags(core::slice::from_ref(&base)); let bases = PyTuple::new_ref_typed_with_type(vec![base.clone()], PyTuple::static_type().to_owned()); let mro = base.mro_map_collect(|x| x.to_owned()); @@ -1127,7 +1098,6 @@ impl PyType { slots, heaptype_ext: None, tp_version_tag: AtomicU32::new(0), - abc_tpflags: AtomicU64::new(inherited_abc_tpflags), }, metaclass, None, @@ -1586,17 +1556,19 @@ impl PyType { /// The type's fully qualified name, the way the `%T` format code prints it: /// `module.qualname`, with a `builtins` or `__main__` module left off. - pub fn fully_qualified_name(&self, vm: &VirtualMachine) -> String { + pub fn fully_qualified_name(&self, vm: &VirtualMachine) -> PyResult { let qualname = self.__qualname__(vm); let qualname = qualname .downcast_ref::() .and_then(|qualname| qualname.to_str()) .map_or_else(|| self.name().to_string(), str::to_owned); - let module = self.__module__(vm); - match module.downcast_ref::().and_then(|m| m.to_str()) { - Some("builtins" | "__main__") | None => qualname, - Some(module) => format!("{module}.{qualname}"), - } + let module = self.__module__(vm)?; + Ok( + match module.downcast_ref::().and_then(|m| m.to_str()) { + Some("builtins" | "__main__") | None => qualname, + Some(module) => format!("{module}.{qualname}"), + }, + ) } pub fn name(&self) -> BorrowedValue<'_, str> { @@ -1722,6 +1694,7 @@ impl Py { } #[pyclass( + itemsize = core::mem::size_of::(), with( Py, Constructor, @@ -1923,12 +1896,6 @@ impl PyType { result } - #[pygetset] - fn __flags__(&self) -> u64 { - self.slots.flags.bits() - | (self.abc_tpflags.load(Ordering::Acquire) & PyTypeFlags::IS_ABSTRACT.bits()) - } - #[pygetset] fn __abstractmethods__(zelf: &Py, vm: &VirtualMachine) -> PyResult { if zelf.is(vm.ctx.types.type_type) { @@ -1970,16 +1937,6 @@ impl PyType { Ok(()) } - #[pygetset] - fn __basicsize__(&self) -> usize { - crate::object::SIZEOF_PYOBJECT_HEAD + self.slots.basicsize - } - - #[pygetset] - fn __itemsize__(&self) -> usize { - self.slots.itemsize - } - #[pygetset] pub fn __name__(&self, vm: &VirtualMachine) -> PyStrRef { self.name_inner( @@ -2243,20 +2200,22 @@ impl PyType { } #[pygetset] - pub fn __module__(&self, vm: &VirtualMachine) -> PyObjectRef { - self.attributes - .get(identifier!(vm, __module__)) - // We need to exclude this method from going into recursion: - .filter(|found| !found.fast_isinstance(vm.ctx.types.getset_type)) - .unwrap_or_else(|| { - // For non-heap types, extract module from tp_name (e.g. "typing.TypeAliasType" -> "typing") - let slot_name = self.slot_name(); - if let Some((module, _)) = slot_name.rsplit_once('.') { - vm.ctx.intern_str(module).to_object() - } else { - vm.ctx.new_str(ascii!("builtins")).into() - } - }) + pub fn __module__(&self, vm: &VirtualMachine) -> PyResult { + // Heap types store the module in the type dict. Static types take the + // text before the last `.` of the type name, or `builtins`. + if self.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) { + return self + .attributes + .get(identifier!(vm, __module__)) + .ok_or_else(|| vm.new_attribute_error("__module__")); + } + let slot_name = self.slot_name(); + let module = if let Some((module, _)) = slot_name.rsplit_once('.') { + vm.ctx.intern_str(module) + } else { + vm.ctx.intern_str("builtins") + }; + Ok(module.to_object()) } #[pygetset(setter)] @@ -2555,10 +2514,10 @@ impl Constructor for PyType { if let Some(globals) = crate::frame::current_globals() { let entry = attributes.entry(identifier!(vm, __module__)); - if matches!(entry, Entry::Vacant(_)) { - let module_name = - vm.unwrap_or_none(globals.get_item_opt(identifier!(vm, __name__), vm)?); - entry.or_insert(module_name); + if let Entry::Vacant(entry) = entry + && let Some(module_name) = globals.get_item_opt(identifier!(vm, __name__), vm)? + { + entry.insert(module_name); } } @@ -2602,14 +2561,10 @@ impl Constructor for PyType { tuple.try_into_typed(vm)? }; - // Check if base has itemsize > 0 - can't add arbitrary slots to variable-size types - // Types like int, bytes, tuple have itemsize > 0 and don't allow custom slots - // But types like weakref.ref have itemsize = 0 and DO allow slots - let has_custom_slots = slots - .as_slice() - .iter() - .any(|s| !matches!(s.as_bytes(), b"__dict__" | b"__weakref__")); - if has_custom_slots && base.slots.itemsize > 0 { + // Any nonempty __slots__ is rejected when the base has a variable + // item size, including a tuple of only `__dict__` or `__weakref__`. + // Types like weakref.ref have itemsize 0 and do allow slots. + if !slots.as_slice().is_empty() && base.slots.itemsize > 0 { return Err(vm.new_type_error(format!( "nonempty __slots__ not supported for subtype of '{}'", base.name() @@ -2726,14 +2681,16 @@ impl Constructor for PyType { // Add HAS_WEAKREF if: // 1. __slots__ is not defined (automatic weakref support), OR // 2. __weakref__ is in __slots__ - let may_add_weakref = !base.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF); + // A variable-size base does not gain a weakref slot. + let may_add_weakref = + base.slots.itemsize == 0 && !base.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF); if (heaptype_slots.is_none() && may_add_weakref) || add_weakref { flags |= PyTypeFlags::HAS_WEAKREF | PyTypeFlags::MANAGED_WEAKREF; } let (slots, heaptype_ext) = { let slots = PyTypeSlots { - flags, + flags: crate::types::PyAtomicTypeFlags::new(flags), member_count, itemsize: base.slots.itemsize, ..PyTypeSlots::heap_default() @@ -3301,8 +3258,12 @@ impl AsNumber for PyType { impl Representable for PyType { #[inline] fn repr_str(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let module = zelf.__module__(vm); - let module = module.downcast_ref::().map(|m| m.as_wtf8()); + // A missing `__module__` is not an error here. + let module = zelf.__module__(vm).ok(); + let module = module + .as_ref() + .and_then(|m| m.downcast_ref::()) + .map(|m| m.as_wtf8()); let repr = match module { Some(module) if module != "builtins" => { @@ -3703,7 +3664,7 @@ fn mro_internal(typ: &Py, vm: &VirtualMachine) -> PyResult { /// Returns true if the two types have different instance layouts. fn shape_differs(t1: &Py, t2: &Py) -> bool { - t1.__basicsize__() != t2.__basicsize__() || t1.slots.itemsize != t2.slots.itemsize + t1.slots.basicsize != t2.slots.basicsize || t1.slots.itemsize != t2.slots.itemsize } fn solid_base<'a>(typ: &'a Py, vm: &VirtualMachine) -> &'a Py { diff --git a/crates/vm/src/class.rs b/crates/vm/src/class.rs index 1bbcda247..ef7efc242 100644 --- a/crates/vm/src/class.rs +++ b/crates/vm/src/class.rs @@ -145,10 +145,16 @@ pub trait StaticType { where Self: PyClassImpl, { + // inherit_special COPYVAL(tp_itemsize): the direct base, and only when + // this type left the slot at 0. The base type object already exists. + let mut slots = Self::make_slots(); + if slots.itemsize == 0 { + slots.itemsize = Self::static_baseclass().slots.itemsize; + } PyType::new_static( Self::static_baseclass().to_owned(), Default::default(), - Self::make_slots(), + slots, Self::static_metaclass().to_owned(), ) .unwrap() @@ -263,7 +269,9 @@ pub trait PyClassImpl: PyClassDef { let _ = ctx.intern_str(Self::NAME); // intern type name - if Self::TP_FLAGS.has_feature(PyTypeFlags::HAS_DICT) { + if Self::TP_FLAGS.has_feature(PyTypeFlags::HAS_DICT) + && !Self::MEMBERS.iter().any(|member| member.name == "__dict__") + { let __dict__ = identifier!(ctx, __dict__); class.set_attr( __dict__, @@ -376,7 +384,7 @@ pub trait PyClassImpl: PyClassDef { fn make_slots() -> PyTypeSlots { let mut slots = PyTypeSlots { - flags: Self::TP_FLAGS, + flags: crate::types::PyAtomicTypeFlags::new(Self::TP_FLAGS), name: Self::TP_NAME, basicsize: Self::BASICSIZE, itemsize: Self::ITEMSIZE, diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index 510580607..5da8c8272 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -657,7 +657,7 @@ pub(crate) fn unraisable_while_closing( if e.traceback().is_none() && let Some(frame) = coro.frame_opt() { - let lasti = frame.lasti().saturating_mul(2); + let lasti = frame.lasti().saturating_mul(2) as i32; let lineno = rustpython_compiler_core::OneIndexed::new(frame.lineno().max(1) as usize) .unwrap_or(rustpython_compiler_core::OneIndexed::MIN); let tb = PyTraceback::new(None, frame, lasti, lineno); diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs index ffac0b387..db9d8f4ea 100644 --- a/crates/vm/src/exception_group.rs +++ b/crates/vm/src/exception_group.rs @@ -18,16 +18,23 @@ use crate::exceptions::types::PyBaseException; fn create_exception_group(ctx: &Context) -> PyRef { let excs = &ctx.exceptions; let exception_group_slots = PyTypeSlots { - flags: PyTypeFlags::heap_type_flags() | PyTypeFlags::HAS_DICT, + flags: crate::types::PyAtomicTypeFlags::new( + PyTypeFlags::heap_type_flags() | PyTypeFlags::HAS_DICT, + ), ..Default::default() }; + let mut attrs = crate::builtins::type_::PyAttributes::default(); + attrs.insert( + crate::identifier!(ctx, __module__), + ctx.intern_str("builtins").to_object(), + ); PyType::new_heap( "ExceptionGroup", vec![ excs.base_exception_group.to_owned(), excs.exception_type.to_owned(), ], - Default::default(), + attrs, exception_group_slots, ctx.types.type_type.to_owned(), ctx, diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 5af8be7b4..96b4f6f63 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -2689,7 +2689,7 @@ pub(crate) fn trampoline_handle_exception( // Add traceback entry at the call site. if let Some((loc, _end_loc)) = exec.code.locations.get(idx) { let next = exception.traceback(); - let new_traceback = PyTraceback::new(next, exec.frame_object(vm), idx as u32 * 2, loc.line); + let new_traceback = PyTraceback::new(next, exec.frame_object(vm), idx as i32 * 2, loc.line); exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx))); } @@ -3608,7 +3608,7 @@ impl ExecutingFrame<'_> { let new_traceback = PyTraceback::new( next, self.frame_object(vm), - idx as u32 * 2, + idx as i32 * 2, loc.line, ); exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx))); @@ -3717,7 +3717,7 @@ impl ExecutingFrame<'_> { let new_traceback = PyTraceback::new( next, frame.frame_object(vm), - idx as u32 * 2, + idx as i32 * 2, loc.line, ); exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx))); @@ -3808,7 +3808,7 @@ impl ExecutingFrame<'_> { let new_traceback = PyTraceback::new( next, frame.frame_object(vm), - idx as u32 * 2, + idx as i32 * 2, loc.line, ); vm_trace!( @@ -4017,7 +4017,7 @@ impl ExecutingFrame<'_> { let (loc, _end_loc) = self.code.locations[idx]; let next = err.traceback(); let new_traceback = - PyTraceback::new(next, self.frame_object(vm), idx as u32 * 2, loc.line); + PyTraceback::new(next, self.frame_object(vm), idx as i32 * 2, loc.line); err.set_traceback(Some(new_traceback.into_ref(&vm.ctx))); } @@ -4077,7 +4077,7 @@ impl ExecutingFrame<'_> { let new_traceback = PyTraceback::new( next, self.frame_object(vm), - idx as u32 * 2, + idx as i32 * 2, loc.line, ); err.set_traceback(Some(new_traceback.into_ref(&vm.ctx))); @@ -4124,7 +4124,7 @@ impl ExecutingFrame<'_> { let (loc, _end_loc) = self.code.locations[idx]; let next = exception.traceback(); let new_traceback = - PyTraceback::new(next, self.frame_object(vm), idx as u32 * 2, loc.line); + PyTraceback::new(next, self.frame_object(vm), idx as i32 * 2, loc.line); exception.set_traceback(Some(new_traceback.into_ref(&vm.ctx))); } diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index 8c57c74c0..db2cb2757 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -158,7 +158,7 @@ impl PyMethodDef { zelf: None, value: self, module_object: None, - module: None, + module: crate::object::PyAtomicRef::new_empty(), _method_def_owner: None, } } @@ -181,7 +181,7 @@ impl PyMethodDef { zelf: Some(obj), value: self, module_object: None, - module: None, + module: crate::object::PyAtomicRef::new_empty(), _method_def_owner: None, }, class, @@ -201,7 +201,7 @@ impl PyMethodDef { zelf: Some(obj), value: self, module_object: None, - module: None, + module: crate::object::PyAtomicRef::new_empty(), _method_def_owner: None, }; PyRef::new_ref( @@ -255,7 +255,7 @@ impl PyMethodDef { zelf: Some(class.to_owned().into()), value: self, module_object: None, - module: None, + module: crate::object::PyAtomicRef::new_empty(), _method_def_owner: None, }; PyNativeMethod { func, class }.into_ref(ctx) diff --git a/crates/vm/src/macros.rs b/crates/vm/src/macros.rs index 84360b9dc..ce4647c60 100644 --- a/crates/vm/src/macros.rs +++ b/crates/vm/src/macros.rs @@ -16,7 +16,7 @@ macro_rules! py_class { { #[allow(unused_mut)] let mut slots = $crate::types::PyTypeSlots::heap_default(); - slots.flags = $flags; + slots.flags = $crate::types::PyAtomicTypeFlags::new($flags); $($crate::py_class!(@extract_slots($ctx, &mut slots, $name, $value));)* let py_class = $ctx.new_class(None, $class_name, $class_base, slots); $($crate::py_class!(@extract_attrs($ctx, &py_class, $name, $value));)* diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index ef3eaa12e..594e206fa 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -514,9 +514,17 @@ impl Py { fn read_type_flags(&self) -> (crate::types::PyTypeFlags, usize) { let typ_ptr = self.typ.load_raw(); let slots = unsafe { core::ptr::addr_of!((*typ_ptr).payload.slots) }; - let flags = unsafe { core::ptr::addr_of!((*slots).flags).read() }; + // SAFETY: `flags` is `PyAtomicTypeFlags`, transparent over `AtomicU64`. + // The type object is live. This load does not form a reference to it. + let bits = unsafe { + (*core::ptr::addr_of!((*slots).flags).cast::()) + .load(core::sync::atomic::Ordering::Acquire) + }; let member_count = unsafe { core::ptr::addr_of!((*slots).member_count).read() }; - (flags, member_count) + ( + crate::types::PyTypeFlags::from_bits_truncate(bits), + member_count, + ) } /// Access the ObjExt prefix at a negative offset from this Py. @@ -1402,9 +1410,14 @@ impl Py { if let Some(offset) = ext_start { let ext_ptr = alloc_ptr.add(offset) as *mut ObjExt; - let flags = typ.slots.flags; - let has_dict = flags.has_feature(crate::types::PyTypeFlags::HAS_DICT); - let inline_values = flags.has_feature(crate::types::PyTypeFlags::INLINE_VALUES); + let has_dict = typ + .slots + .flags + .has_feature(crate::types::PyTypeFlags::HAS_DICT); + let inline_values = typ + .slots + .flags + .has_feature(crate::types::PyTypeFlags::INLINE_VALUES); ext_ptr.write(ObjExt::new(dict, has_dict, inline_values)); } @@ -3118,7 +3131,6 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy { slots: PyType::make_slots(), heaptype_ext: None, tp_version_tag: core::sync::atomic::AtomicU32::new(0), - abc_tpflags: core::sync::atomic::AtomicU64::new(0), }; let object_payload = PyType { base: unsafe { PyAtomicRef::from_optional_ref_without_retag(None) }, @@ -3129,7 +3141,6 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy { slots: object::PyBaseObject::make_slots(), heaptype_ext: None, tp_version_tag: core::sync::atomic::AtomicU32::new(0), - abc_tpflags: core::sync::atomic::AtomicU64::new(0), }; let tuple_payload = PyType { base: unsafe { @@ -3144,7 +3155,6 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy { slots: tuple::PyTuple::make_slots(), heaptype_ext: None, tp_version_tag: core::sync::atomic::AtomicU32::new(0), - abc_tpflags: core::sync::atomic::AtomicU64::new(0), }; let object_element = @@ -3192,7 +3202,6 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy { slots: PyWeak::make_slots(), heaptype_ext: None, tp_version_tag: core::sync::atomic::AtomicU32::new(0), - abc_tpflags: core::sync::atomic::AtomicU64::new(0), }; let weakref_type = PyRef::new_ref(weakref_payload, type_type.clone(), None); // Static type: untrack from GC (was tracked by new_ref because PyType has HAS_TRAVERSE) diff --git a/crates/vm/src/object/ext.rs b/crates/vm/src/object/ext.rs index c0b7a66db..ed95fe834 100644 --- a/crates/vm/src/object/ext.rs +++ b/crates/vm/src/object/ext.rs @@ -252,6 +252,7 @@ const _: () = assert!( == core::mem::align_of::>() && core::mem::offset_of!(PyAtomicRef>, inner) == core::mem::offset_of!(PyAtomicRef<()>, inner) + && core::mem::offset_of!(PyAtomicRef>, inner) == 0 ); impl Drop for PyAtomicRef { @@ -367,6 +368,32 @@ impl PyAtomicRef { frame.iframe().cold().temporary_refs.lock().push(old.into()); } } + + /// Strong reference to the current value. + /// + /// The cell is never null. A concurrent store may drop the previous value; + /// the incref is retried until it applies to the pointer still in the slot. + /// A null load is retried rather than forged. + pub(crate) fn load_owned(&self) -> PyRef { + loop { + if let Some(obj) = cell_load_owned(&self.inner) { + // SAFETY: this cell is only stored with `PyRef`. + return unsafe { obj.downcast_unchecked() }; + } + core::hint::spin_loop(); + } + } + + /// Replace the stored reference. Returns the previous one, still owned. + /// + /// `None` only if the cell was empty. Callers that publish through + /// `From>` and `store` keep a `T` in the slot. + pub(crate) fn store(&self, value: PyRef) -> Option> { + cell_store(&self.inner, Some(value.into())).map(|obj| { + // SAFETY: this cell is only stored with `PyRef`. + unsafe { obj.downcast_unchecked() } + }) + } } impl From>> for PyAtomicRef> { @@ -460,13 +487,6 @@ impl PyAtomicRef> { } } -fn empty_cell() -> PyAtomicRef { - PyAtomicRef { - inner: Radium::new(null_mut()), - _phantom: PhantomData, - } -} - fn cell_load_ptr(inner: &PyAtomic<*mut u8>) -> *mut PyObject { inner.load(Ordering::Acquire).cast() } @@ -629,8 +649,20 @@ impl From> for PyAtomicRef> { impl PyAtomicRef> { /// Empty slot. The pointer is null and owns no reference. - pub(crate) fn new_empty() -> Self { - empty_cell() + pub(crate) const fn new_empty() -> Self { + Self { + inner: { + #[cfg(feature = "threading")] + { + core::sync::atomic::AtomicPtr::new(null_mut()) + } + #[cfg(not(feature = "threading"))] + { + core::cell::Cell::new(null_mut()) + } + }, + _phantom: PhantomData, + } } /// Borrowed pointer currently stored. Null when the slot is empty. diff --git a/crates/vm/src/object/traverse.rs b/crates/vm/src/object/traverse.rs index 0f7b13d16..e0a7ae552 100644 --- a/crates/vm/src/object/traverse.rs +++ b/crates/vm/src/object/traverse.rs @@ -125,6 +125,13 @@ unsafe impl Traverse for super::ext::PyAtomicRef> } } +unsafe impl Traverse for super::ext::PyAtomicRef { + #[inline] + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + traverse_fn(self.as_object()); + } +} + unsafe impl Traverse for super::ext::PyAtomicRef { #[inline] fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { diff --git a/crates/vm/src/stdlib/_abc.rs b/crates/vm/src/stdlib/_abc.rs index e759e2fc4..f4e109072 100644 --- a/crates/vm/src/stdlib/_abc.rs +++ b/crates/vm/src/stdlib/_abc.rs @@ -252,9 +252,7 @@ mod _abc { // _abc_register propagates Py_TPFLAGS_SEQUENCE/MAPPING // recursively so MATCH_SEQUENCE/MATCH_MAPPING see ABC registration. let collection_mask = PyTypeFlags::SEQUENCE | PyTypeFlags::MAPPING; - let collection_flags = (cls_type.slots.flags - | PyTypeFlags::from_bits_truncate(cls_type.abc_tpflags.load(Ordering::Acquire))) - & collection_mask; + let collection_flags = cls_type.slots.flags.load() & collection_mask; if !subclass_type.is(vm.ctx.types.str_type) && !subclass_type.is(vm.ctx.types.bytes_type) && !subclass_type.is(vm.ctx.types.bytearray_type) diff --git a/crates/vm/src/stdlib/_ast/pyast.rs b/crates/vm/src/stdlib/_ast/pyast.rs index a9a2f97c2..46bf0918d 100644 --- a/crates/vm/src/stdlib/_ast/pyast.rs +++ b/crates/vm/src/stdlib/_ast/pyast.rs @@ -83,13 +83,8 @@ macro_rules! impl_base_node { #[extend_class] fn extend_class(ctx: &Context, class: &'static Py) { - // AST types are mutable (heap types in CPython, not IMMUTABLETYPE) - // Safety: called during type initialization before any concurrent access - unsafe { - let flags = &class.slots.flags as *const crate::types::PyTypeFlags - as *mut crate::types::PyTypeFlags; - (*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE); - } + // AST types are mutable (heap types, not IMMUTABLETYPE). + class.slots.flags.remove(crate::types::PyTypeFlags::IMMUTABLETYPE); class.set_attr( identifier!(ctx, _fields), ctx.empty_tuple.clone().into(), @@ -132,13 +127,8 @@ macro_rules! impl_base_node { #[extend_class] fn extend_class_with_fields(ctx: &Context, class: &'static Py) { - // AST types are mutable (heap types in CPython, not IMMUTABLETYPE) - // Safety: called during type initialization before any concurrent access - unsafe { - let flags = &class.slots.flags as *const crate::types::PyTypeFlags - as *mut crate::types::PyTypeFlags; - (*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE); - } + // AST types are mutable (heap types, not IMMUTABLETYPE). + class.slots.flags.remove(crate::types::PyTypeFlags::IMMUTABLETYPE); class.set_attr( identifier!(ctx, _fields), ctx.new_tuple(vec![ @@ -179,30 +169,30 @@ macro_rules! impl_base_node { }; } -#[pyclass(module = "_ast", name = "mod", base = NodeAst)] +#[pyclass(module = "ast", name = "mod", base = NodeAst)] pub(crate) struct NodeMod(NodeAst); impl_base_node!(NodeMod); impl_node!( - #[pyclass(module = "_ast", name = "Module", base = NodeMod)] + #[pyclass(module = "ast", name = "Module", base = NodeMod)] pub(crate) struct NodeModModule, fields: ["body", "type_ignores"], ); impl_node!( - #[pyclass(module = "_ast", name = "Interactive", base = NodeMod)] + #[pyclass(module = "ast", name = "Interactive", base = NodeMod)] pub(crate) struct NodeModInteractive, fields: ["body"], ); impl_node!( - #[pyclass(module = "_ast", name = "Expression", base = NodeMod)] + #[pyclass(module = "ast", name = "Expression", base = NodeMod)] pub(crate) struct NodeModExpression, fields: ["body"], ); -#[pyclass(module = "_ast", name = "stmt", base = NodeAst)] +#[pyclass(module = "ast", name = "stmt", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeStmt(NodeAst); @@ -213,199 +203,199 @@ impl_base_node!( ); impl_node!( - #[pyclass(module = "_ast", name = "FunctionType", base = NodeMod)] + #[pyclass(module = "ast", name = "FunctionType", base = NodeMod)] pub(crate) struct NodeModFunctionType, fields: ["argtypes", "returns"], ); impl_node!( - #[pyclass(module = "_ast", name = "FunctionDef", base = NodeStmt)] + #[pyclass(module = "ast", name = "FunctionDef", base = NodeStmt)] pub(crate) struct NodeStmtFunctionDef, fields: ["name", "args", "body", "decorator_list", "returns", "type_comment", "type_params"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "AsyncFunctionDef", base = NodeStmt)] + #[pyclass(module = "ast", name = "AsyncFunctionDef", base = NodeStmt)] pub(crate) struct NodeStmtAsyncFunctionDef, fields: ["name", "args", "body", "decorator_list", "returns", "type_comment", "type_params"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "ClassDef", base = NodeStmt)] + #[pyclass(module = "ast", name = "ClassDef", base = NodeStmt)] pub(crate) struct NodeStmtClassDef, fields: ["name", "bases", "keywords", "body", "decorator_list", "type_params"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Return", base = NodeStmt)] + #[pyclass(module = "ast", name = "Return", base = NodeStmt)] pub(crate) struct NodeStmtReturn, fields: ["value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Delete", base = NodeStmt)] + #[pyclass(module = "ast", name = "Delete", base = NodeStmt)] pub(crate) struct NodeStmtDelete, fields: ["targets"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Assign", base = NodeStmt)] + #[pyclass(module = "ast", name = "Assign", base = NodeStmt)] pub(crate) struct NodeStmtAssign, fields: ["targets", "value", "type_comment"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "TypeAlias", base = NodeStmt)] + #[pyclass(module = "ast", name = "TypeAlias", base = NodeStmt)] pub(crate) struct NodeStmtTypeAlias, fields: ["name", "type_params", "value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "AugAssign", base = NodeStmt)] + #[pyclass(module = "ast", name = "AugAssign", base = NodeStmt)] pub(crate) struct NodeStmtAugAssign, fields: ["target", "op", "value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "AnnAssign", base = NodeStmt)] + #[pyclass(module = "ast", name = "AnnAssign", base = NodeStmt)] pub(crate) struct NodeStmtAnnAssign, fields: ["target", "annotation", "value", "simple"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "For", base = NodeStmt)] + #[pyclass(module = "ast", name = "For", base = NodeStmt)] pub(crate) struct NodeStmtFor, fields: ["target", "iter", "body", "orelse", "type_comment"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "AsyncFor", base = NodeStmt)] + #[pyclass(module = "ast", name = "AsyncFor", base = NodeStmt)] pub(crate) struct NodeStmtAsyncFor, fields: ["target", "iter", "body", "orelse", "type_comment"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "While", base = NodeStmt)] + #[pyclass(module = "ast", name = "While", base = NodeStmt)] pub(crate) struct NodeStmtWhile, fields: ["test", "body", "orelse"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "If", base = NodeStmt)] + #[pyclass(module = "ast", name = "If", base = NodeStmt)] pub(crate) struct NodeStmtIf, fields: ["test", "body", "orelse"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "With", base = NodeStmt)] + #[pyclass(module = "ast", name = "With", base = NodeStmt)] pub(crate) struct NodeStmtWith, fields: ["items", "body", "type_comment"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "AsyncWith", base = NodeStmt)] + #[pyclass(module = "ast", name = "AsyncWith", base = NodeStmt)] pub(crate) struct NodeStmtAsyncWith, fields: ["items", "body", "type_comment"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Match", base = NodeStmt)] + #[pyclass(module = "ast", name = "Match", base = NodeStmt)] pub(crate) struct NodeStmtMatch, fields: ["subject", "cases"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Raise", base = NodeStmt)] + #[pyclass(module = "ast", name = "Raise", base = NodeStmt)] pub(crate) struct NodeStmtRaise, fields: ["exc", "cause"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Try", base = NodeStmt)] + #[pyclass(module = "ast", name = "Try", base = NodeStmt)] pub(crate) struct NodeStmtTry, fields: ["body", "handlers", "orelse", "finalbody"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "TryStar", base = NodeStmt)] + #[pyclass(module = "ast", name = "TryStar", base = NodeStmt)] pub(crate) struct NodeStmtTryStar, fields: ["body", "handlers", "orelse", "finalbody"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Assert", base = NodeStmt)] + #[pyclass(module = "ast", name = "Assert", base = NodeStmt)] pub(crate) struct NodeStmtAssert, fields: ["test", "msg"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Import", base = NodeStmt)] + #[pyclass(module = "ast", name = "Import", base = NodeStmt)] pub(crate) struct NodeStmtImport, fields: ["names"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "ImportFrom", base = NodeStmt)] + #[pyclass(module = "ast", name = "ImportFrom", base = NodeStmt)] pub(crate) struct NodeStmtImportFrom, fields: ["module", "names", "level"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Global", base = NodeStmt)] + #[pyclass(module = "ast", name = "Global", base = NodeStmt)] pub(crate) struct NodeStmtGlobal, fields: ["names"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Nonlocal", base = NodeStmt)] + #[pyclass(module = "ast", name = "Nonlocal", base = NodeStmt)] pub(crate) struct NodeStmtNonlocal, fields: ["names"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Expr", base = NodeStmt)] + #[pyclass(module = "ast", name = "Expr", base = NodeStmt)] pub(crate) struct NodeStmtExpr, fields: ["value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Pass", base = NodeStmt)] + #[pyclass(module = "ast", name = "Pass", base = NodeStmt)] pub(crate) struct NodeStmtPass, attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Break", base = NodeStmt)] + #[pyclass(module = "ast", name = "Break", base = NodeStmt)] pub(crate) struct NodeStmtBreak, attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); -#[pyclass(module = "_ast", name = "expr", base = NodeAst)] +#[pyclass(module = "ast", name = "expr", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeExpr(NodeAst); @@ -416,159 +406,159 @@ impl_base_node!( ); impl_node!( - #[pyclass(module = "_ast", name = "Continue", base = NodeStmt)] + #[pyclass(module = "ast", name = "Continue", base = NodeStmt)] pub(crate) struct NodeStmtContinue, attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "BoolOp", base = NodeExpr)] + #[pyclass(module = "ast", name = "BoolOp", base = NodeExpr)] pub(crate) struct NodeExprBoolOp, fields: ["op", "values"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "NamedExpr", base = NodeExpr)] + #[pyclass(module = "ast", name = "NamedExpr", base = NodeExpr)] pub(crate) struct NodeExprNamedExpr, fields: ["target", "value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "BinOp", base = NodeExpr)] + #[pyclass(module = "ast", name = "BinOp", base = NodeExpr)] pub(crate) struct NodeExprBinOp, fields: ["left", "op", "right"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "UnaryOp", base = NodeExpr)] + #[pyclass(module = "ast", name = "UnaryOp", base = NodeExpr)] pub(crate) struct NodeExprUnaryOp, fields: ["op", "operand"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Lambda", base = NodeExpr)] + #[pyclass(module = "ast", name = "Lambda", base = NodeExpr)] pub(crate) struct NodeExprLambda, fields: ["args", "body"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "IfExp", base = NodeExpr)] + #[pyclass(module = "ast", name = "IfExp", base = NodeExpr)] pub(crate) struct NodeExprIfExp, fields: ["test", "body", "orelse"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Dict", base = NodeExpr)] + #[pyclass(module = "ast", name = "Dict", base = NodeExpr)] pub(crate) struct NodeExprDict, fields: ["keys", "values"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Set", base = NodeExpr)] + #[pyclass(module = "ast", name = "Set", base = NodeExpr)] pub(crate) struct NodeExprSet, fields: ["elts"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "ListComp", base = NodeExpr)] + #[pyclass(module = "ast", name = "ListComp", base = NodeExpr)] pub(crate) struct NodeExprListComp, fields: ["elt", "generators"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "SetComp", base = NodeExpr)] + #[pyclass(module = "ast", name = "SetComp", base = NodeExpr)] pub(crate) struct NodeExprSetComp, fields: ["elt", "generators"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "DictComp", base = NodeExpr)] + #[pyclass(module = "ast", name = "DictComp", base = NodeExpr)] pub(crate) struct NodeExprDictComp, fields: ["key", "value", "generators"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "GeneratorExp", base = NodeExpr)] + #[pyclass(module = "ast", name = "GeneratorExp", base = NodeExpr)] pub(crate) struct NodeExprGeneratorExp, fields: ["elt", "generators"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Await", base = NodeExpr)] + #[pyclass(module = "ast", name = "Await", base = NodeExpr)] pub(crate) struct NodeExprAwait, fields: ["value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Yield", base = NodeExpr)] + #[pyclass(module = "ast", name = "Yield", base = NodeExpr)] pub(crate) struct NodeExprYield, fields: ["value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "YieldFrom", base = NodeExpr)] + #[pyclass(module = "ast", name = "YieldFrom", base = NodeExpr)] pub(crate) struct NodeExprYieldFrom, fields: ["value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Compare", base = NodeExpr)] + #[pyclass(module = "ast", name = "Compare", base = NodeExpr)] pub(crate) struct NodeExprCompare, fields: ["left", "ops", "comparators"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Call", base = NodeExpr)] + #[pyclass(module = "ast", name = "Call", base = NodeExpr)] pub(crate) struct NodeExprCall, fields: ["func", "args", "keywords"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "FormattedValue", base = NodeExpr)] + #[pyclass(module = "ast", name = "FormattedValue", base = NodeExpr)] pub(crate) struct NodeExprFormattedValue, fields: ["value", "conversion", "format_spec"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "JoinedStr", base = NodeExpr)] + #[pyclass(module = "ast", name = "JoinedStr", base = NodeExpr)] pub(crate) struct NodeExprJoinedStr, fields: ["values"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "TemplateStr", base = NodeExpr)] + #[pyclass(module = "ast", name = "TemplateStr", base = NodeExpr)] pub(crate) struct NodeExprTemplateStr, fields: ["values"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Interpolation", base = NodeExpr)] + #[pyclass(module = "ast", name = "Interpolation", base = NodeExpr)] pub(crate) struct NodeExprInterpolation, fields: ["value", "str", "conversion", "format_spec"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); -#[pyclass(module = "_ast", name = "Constant", base = NodeExpr)] +#[pyclass(module = "ast", name = "Constant", base = NodeExpr)] #[repr(transparent)] pub(crate) struct NodeExprConstant(NodeExpr); @@ -576,13 +566,11 @@ pub(crate) struct NodeExprConstant(NodeExpr); impl NodeExprConstant { #[extend_class] fn extend_class_with_fields(ctx: &Context, class: &'static Py) { - // AST types are mutable (heap types, not IMMUTABLETYPE) - // Safety: called during type initialization before any concurrent access - unsafe { - let flags = &class.slots.flags as *const crate::types::PyTypeFlags - as *mut crate::types::PyTypeFlags; - (*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE); - } + // AST types are mutable (heap types, not IMMUTABLETYPE). + class + .slots + .flags + .remove(crate::types::PyTypeFlags::IMMUTABLETYPE); class.set_attr( identifier!(ctx, _fields), ctx.new_tuple(vec![ @@ -615,245 +603,245 @@ impl NodeExprConstant { } impl_node!( - #[pyclass(module = "_ast", name = "Attribute", base = NodeExpr)] + #[pyclass(module = "ast", name = "Attribute", base = NodeExpr)] pub(crate) struct NodeExprAttribute, fields: ["value", "attr", "ctx"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Subscript", base = NodeExpr)] + #[pyclass(module = "ast", name = "Subscript", base = NodeExpr)] pub(crate) struct NodeExprSubscript, fields: ["value", "slice", "ctx"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Starred", base = NodeExpr)] + #[pyclass(module = "ast", name = "Starred", base = NodeExpr)] pub(crate) struct NodeExprStarred, fields: ["value", "ctx"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Name", base = NodeExpr)] + #[pyclass(module = "ast", name = "Name", base = NodeExpr)] pub(crate) struct NodeExprName, fields: ["id", "ctx"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "List", base = NodeExpr)] + #[pyclass(module = "ast", name = "List", base = NodeExpr)] pub(crate) struct NodeExprList, fields: ["elts", "ctx"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Tuple", base = NodeExpr)] + #[pyclass(module = "ast", name = "Tuple", base = NodeExpr)] pub(crate) struct NodeExprTuple, fields: ["elts", "ctx"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); -#[pyclass(module = "_ast", name = "expr_context", base = NodeAst)] +#[pyclass(module = "ast", name = "expr_context", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeExprContext(NodeAst); impl_base_node!(NodeExprContext); impl_node!( - #[pyclass(module = "_ast", name = "Slice", base = NodeExpr)] + #[pyclass(module = "ast", name = "Slice", base = NodeExpr)] pub(crate) struct NodeExprSlice, fields: ["lower", "upper", "step"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "Load", base = NodeExprContext)] + #[pyclass(module = "ast", name = "Load", base = NodeExprContext)] pub(crate) struct NodeExprContextLoad, ); impl_node!( - #[pyclass(module = "_ast", name = "Store", base = NodeExprContext)] + #[pyclass(module = "ast", name = "Store", base = NodeExprContext)] pub(crate) struct NodeExprContextStore, ); -#[pyclass(module = "_ast", name = "boolop", base = NodeAst)] +#[pyclass(module = "ast", name = "boolop", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeBoolOp(NodeAst); impl_base_node!(NodeBoolOp); impl_node!( - #[pyclass(module = "_ast", name = "Del", base = NodeExprContext)] + #[pyclass(module = "ast", name = "Del", base = NodeExprContext)] pub(crate) struct NodeExprContextDel, ); impl_node!( - #[pyclass(module = "_ast", name = "And", base = NodeBoolOp)] + #[pyclass(module = "ast", name = "And", base = NodeBoolOp)] pub(crate) struct NodeBoolOpAnd, ); -#[pyclass(module = "_ast", name = "operator", base = NodeAst)] +#[pyclass(module = "ast", name = "operator", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeOperator(NodeAst); impl_base_node!(NodeOperator); impl_node!( - #[pyclass(module = "_ast", name = "Or", base = NodeBoolOp)] + #[pyclass(module = "ast", name = "Or", base = NodeBoolOp)] pub(crate) struct NodeBoolOpOr, ); impl_node!( - #[pyclass(module = "_ast", name = "Add", base = NodeOperator)] + #[pyclass(module = "ast", name = "Add", base = NodeOperator)] pub(crate) struct NodeOperatorAdd, ); impl_node!( - #[pyclass(module = "_ast", name = "Sub", base = NodeOperator)] + #[pyclass(module = "ast", name = "Sub", base = NodeOperator)] pub(crate) struct NodeOperatorSub, ); impl_node!( - #[pyclass(module = "_ast", name = "Mult", base = NodeOperator)] + #[pyclass(module = "ast", name = "Mult", base = NodeOperator)] pub(crate) struct NodeOperatorMult, ); impl_node!( - #[pyclass(module = "_ast", name = "MatMult", base = NodeOperator)] + #[pyclass(module = "ast", name = "MatMult", base = NodeOperator)] pub(crate) struct NodeOperatorMatMult, ); impl_node!( - #[pyclass(module = "_ast", name = "Div", base = NodeOperator)] + #[pyclass(module = "ast", name = "Div", base = NodeOperator)] pub(crate) struct NodeOperatorDiv, ); impl_node!( - #[pyclass(module = "_ast", name = "Mod", base = NodeOperator)] + #[pyclass(module = "ast", name = "Mod", base = NodeOperator)] pub(crate) struct NodeOperatorMod, ); impl_node!( - #[pyclass(module = "_ast", name = "Pow", base = NodeOperator)] + #[pyclass(module = "ast", name = "Pow", base = NodeOperator)] pub(crate) struct NodeOperatorPow, ); impl_node!( - #[pyclass(module = "_ast", name = "LShift", base = NodeOperator)] + #[pyclass(module = "ast", name = "LShift", base = NodeOperator)] pub(crate) struct NodeOperatorLShift, ); impl_node!( - #[pyclass(module = "_ast", name = "RShift", base = NodeOperator)] + #[pyclass(module = "ast", name = "RShift", base = NodeOperator)] pub(crate) struct NodeOperatorRShift, ); impl_node!( - #[pyclass(module = "_ast", name = "BitOr", base = NodeOperator)] + #[pyclass(module = "ast", name = "BitOr", base = NodeOperator)] pub(crate) struct NodeOperatorBitOr, ); impl_node!( - #[pyclass(module = "_ast", name = "BitXor", base = NodeOperator)] + #[pyclass(module = "ast", name = "BitXor", base = NodeOperator)] pub(crate) struct NodeOperatorBitXor, ); impl_node!( - #[pyclass(module = "_ast", name = "BitAnd", base = NodeOperator)] + #[pyclass(module = "ast", name = "BitAnd", base = NodeOperator)] pub(crate) struct NodeOperatorBitAnd, ); -#[pyclass(module = "_ast", name = "unaryop", base = NodeAst)] +#[pyclass(module = "ast", name = "unaryop", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeUnaryOp(NodeAst); impl_base_node!(NodeUnaryOp); impl_node!( - #[pyclass(module = "_ast", name = "FloorDiv", base = NodeOperator)] + #[pyclass(module = "ast", name = "FloorDiv", base = NodeOperator)] pub(crate) struct NodeOperatorFloorDiv, ); impl_node!( - #[pyclass(module = "_ast", name = "Invert", base = NodeUnaryOp)] + #[pyclass(module = "ast", name = "Invert", base = NodeUnaryOp)] pub(crate) struct NodeUnaryOpInvert, ); impl_node!( - #[pyclass(module = "_ast", name = "Not", base = NodeUnaryOp)] + #[pyclass(module = "ast", name = "Not", base = NodeUnaryOp)] pub(crate) struct NodeUnaryOpNot, ); impl_node!( - #[pyclass(module = "_ast", name = "UAdd", base = NodeUnaryOp)] + #[pyclass(module = "ast", name = "UAdd", base = NodeUnaryOp)] pub(crate) struct NodeUnaryOpUAdd, ); -#[pyclass(module = "_ast", name = "cmpop", base = NodeAst)] +#[pyclass(module = "ast", name = "cmpop", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeCmpOp(NodeAst); impl_base_node!(NodeCmpOp); impl_node!( - #[pyclass(module = "_ast", name = "USub", base = NodeUnaryOp)] + #[pyclass(module = "ast", name = "USub", base = NodeUnaryOp)] pub(crate) struct NodeUnaryOpUSub, ); impl_node!( - #[pyclass(module = "_ast", name = "Eq", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "Eq", base = NodeCmpOp)] pub(crate) struct NodeCmpOpEq, ); impl_node!( - #[pyclass(module = "_ast", name = "NotEq", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "NotEq", base = NodeCmpOp)] pub(crate) struct NodeCmpOpNotEq, ); impl_node!( - #[pyclass(module = "_ast", name = "Lt", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "Lt", base = NodeCmpOp)] pub(crate) struct NodeCmpOpLt, ); impl_node!( - #[pyclass(module = "_ast", name = "LtE", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "LtE", base = NodeCmpOp)] pub(crate) struct NodeCmpOpLtE, ); impl_node!( - #[pyclass(module = "_ast", name = "Gt", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "Gt", base = NodeCmpOp)] pub(crate) struct NodeCmpOpGt, ); impl_node!( - #[pyclass(module = "_ast", name = "GtE", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "GtE", base = NodeCmpOp)] pub(crate) struct NodeCmpOpGtE, ); impl_node!( - #[pyclass(module = "_ast", name = "Is", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "Is", base = NodeCmpOp)] pub(crate) struct NodeCmpOpIs, ); impl_node!( - #[pyclass(module = "_ast", name = "IsNot", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "IsNot", base = NodeCmpOp)] pub(crate) struct NodeCmpOpIsNot, ); impl_node!( - #[pyclass(module = "_ast", name = "In", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "In", base = NodeCmpOp)] pub(crate) struct NodeCmpOpIn, ); impl_node!( - #[pyclass(module = "_ast", name = "NotIn", base = NodeCmpOp)] + #[pyclass(module = "ast", name = "NotIn", base = NodeCmpOp)] pub(crate) struct NodeCmpOpNotIn, ); -#[pyclass(module = "_ast", name = "excepthandler", base = NodeAst)] +#[pyclass(module = "ast", name = "excepthandler", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeExceptHandler(NodeAst); @@ -864,52 +852,52 @@ impl_base_node!( ); impl_node!( - #[pyclass(module = "_ast", name = "comprehension", base = NodeAst)] + #[pyclass(module = "ast", name = "comprehension", base = NodeAst)] pub(crate) struct NodeComprehension, fields: ["target", "iter", "ifs", "is_async"], ); impl_node!( - #[pyclass(module = "_ast", name = "ExceptHandler", base = NodeExceptHandler)] + #[pyclass(module = "ast", name = "ExceptHandler", base = NodeExceptHandler)] pub(crate) struct NodeExceptHandlerExceptHandler, fields: ["type", "name", "body"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "arguments", base = NodeAst)] + #[pyclass(module = "ast", name = "arguments", base = NodeAst)] pub(crate) struct NodeArguments, fields: ["posonlyargs", "args", "vararg", "kwonlyargs", "kw_defaults", "kwarg", "defaults"], ); impl_node!( - #[pyclass(module = "_ast", name = "arg", base = NodeAst)] + #[pyclass(module = "ast", name = "arg", base = NodeAst)] pub(crate) struct NodeArg, fields: ["arg", "annotation", "type_comment"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "keyword", base = NodeAst)] + #[pyclass(module = "ast", name = "keyword", base = NodeAst)] pub(crate) struct NodeKeyword, fields: ["arg", "value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "alias", base = NodeAst)] + #[pyclass(module = "ast", name = "alias", base = NodeAst)] pub(crate) struct NodeAlias, fields: ["name", "asname"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "withitem", base = NodeAst)] + #[pyclass(module = "ast", name = "withitem", base = NodeAst)] pub(crate) struct NodeWithItem, fields: ["context_expr", "optional_vars"], ); -#[pyclass(module = "_ast", name = "pattern", base = NodeAst)] +#[pyclass(module = "ast", name = "pattern", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodePattern(NodeAst); @@ -919,74 +907,74 @@ impl_base_node!( ); impl_node!( - #[pyclass(module = "_ast", name = "match_case", base = NodeAst)] + #[pyclass(module = "ast", name = "match_case", base = NodeAst)] pub(crate) struct NodeMatchCase, fields: ["pattern", "guard", "body"], ); impl_node!( - #[pyclass(module = "_ast", name = "MatchValue", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchValue", base = NodePattern)] pub(crate) struct NodePatternMatchValue, fields: ["value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "MatchSingleton", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchSingleton", base = NodePattern)] pub(crate) struct NodePatternMatchSingleton, fields: ["value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "MatchSequence", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchSequence", base = NodePattern)] pub(crate) struct NodePatternMatchSequence, fields: ["patterns"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "MatchMapping", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchMapping", base = NodePattern)] pub(crate) struct NodePatternMatchMapping, fields: ["keys", "patterns", "rest"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "MatchClass", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchClass", base = NodePattern)] pub(crate) struct NodePatternMatchClass, fields: ["cls", "patterns", "kwd_attrs", "kwd_patterns"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "MatchStar", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchStar", base = NodePattern)] pub(crate) struct NodePatternMatchStar, fields: ["name"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "MatchAs", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchAs", base = NodePattern)] pub(crate) struct NodePatternMatchAs, fields: ["pattern", "name"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); -#[pyclass(module = "_ast", name = "type_ignore", base = NodeAst)] +#[pyclass(module = "ast", name = "type_ignore", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeTypeIgnore(NodeAst); impl_base_node!(NodeTypeIgnore); impl_node!( - #[pyclass(module = "_ast", name = "MatchOr", base = NodePattern)] + #[pyclass(module = "ast", name = "MatchOr", base = NodePattern)] pub(crate) struct NodePatternMatchOr, fields: ["patterns"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); -#[pyclass(module = "_ast", name = "type_param", base = NodeAst)] +#[pyclass(module = "ast", name = "type_param", base = NodeAst)] #[repr(transparent)] pub(crate) struct NodeTypeParam(NodeAst); @@ -996,27 +984,27 @@ impl_base_node!( ); impl_node!( - #[pyclass(module = "_ast", name = "TypeIgnore", base = NodeTypeIgnore)] + #[pyclass(module = "ast", name = "TypeIgnore", base = NodeTypeIgnore)] pub(crate) struct NodeTypeIgnoreTypeIgnore, fields: ["lineno", "tag"], ); impl_node!( - #[pyclass(module = "_ast", name = "TypeVar", base = NodeTypeParam)] + #[pyclass(module = "ast", name = "TypeVar", base = NodeTypeParam)] pub(crate) struct NodeTypeParamTypeVar, fields: ["name", "bound", "default_value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "ParamSpec", base = NodeTypeParam)] + #[pyclass(module = "ast", name = "ParamSpec", base = NodeTypeParam)] pub(crate) struct NodeTypeParamParamSpec, fields: ["name", "default_value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], ); impl_node!( - #[pyclass(module = "_ast", name = "TypeVarTuple", base = NodeTypeParam)] + #[pyclass(module = "ast", name = "TypeVarTuple", base = NodeTypeParam)] pub(crate) struct NodeTypeParamTypeVarTuple, fields: ["name", "default_value"], attributes: ["lineno", "col_offset", "end_lineno", "end_col_offset"], diff --git a/crates/vm/src/stdlib/_ast/python.rs b/crates/vm/src/stdlib/_ast/python.rs index 772135103..e4a5a1fe0 100644 --- a/crates/vm/src/stdlib/_ast/python.rs +++ b/crates/vm/src/stdlib/_ast/python.rs @@ -16,7 +16,7 @@ pub(crate) mod _ast { warn, }; #[pyattr] - #[pyclass(module = "_ast", name = "AST")] + #[pyclass(module = "ast", name = "AST")] #[derive(Debug, PyPayload)] pub(crate) struct NodeAst; @@ -24,13 +24,11 @@ pub(crate) mod _ast { impl NodeAst { #[extend_class] fn extend_class(ctx: &Context, class: &'static Py) { - // AST types are mutable (heap types, not IMMUTABLETYPE) - // Safety: called during type initialization before any concurrent access - unsafe { - let flags = &class.slots.flags as *const crate::types::PyTypeFlags - as *mut crate::types::PyTypeFlags; - (*flags).remove(crate::types::PyTypeFlags::IMMUTABLETYPE); - } + // AST types are mutable (heap types, not IMMUTABLETYPE). + class + .slots + .flags + .remove(crate::types::PyTypeFlags::IMMUTABLETYPE); let empty_tuple = ctx.empty_tuple.clone(); class.set_str_attr("_fields", empty_tuple.clone(), ctx); class.set_str_attr("_attributes", empty_tuple.clone(), ctx); diff --git a/crates/vm/src/stdlib/_ctypes.rs b/crates/vm/src/stdlib/_ctypes.rs index f89742a32..8ba26c1ab 100644 --- a/crates/vm/src/stdlib/_ctypes.rs +++ b/crates/vm/src/stdlib/_ctypes.rs @@ -312,9 +312,9 @@ pub(crate) mod _ctypes { // Create slots with IMMUTABLETYPE flag let slots = PyTypeSlots { name: "COMError", - flags: PyTypeFlags::heap_type_flags() - | PyTypeFlags::HAS_DICT - | PyTypeFlags::IMMUTABLETYPE, + flags: crate::types::PyAtomicTypeFlags::new( + PyTypeFlags::heap_type_flags() | PyTypeFlags::HAS_DICT | PyTypeFlags::IMMUTABLETYPE, + ), ..PyTypeSlots::default() }; diff --git a/crates/vm/src/stdlib/_ctypes/function.rs b/crates/vm/src/stdlib/_ctypes/function.rs index d99ceadae..c16f2a7f3 100644 --- a/crates/vm/src/stdlib/_ctypes/function.rs +++ b/crates/vm/src/stdlib/_ctypes/function.rs @@ -2004,7 +2004,17 @@ impl PyCThunk { unsafe impl Send for PyCThunk {} unsafe impl Sync for PyCThunk {} -#[pyclass] +/// `ffi_type`: `size_t` size, two `unsigned short`s, then a pointer. +#[repr(C)] +#[allow(dead_code)] +struct FfiTypeLayout { + _size: usize, + _alignment: u16, + _type_code: u16, + _elements: *mut *mut u8, +} + +#[pyclass(itemsize = core::mem::size_of::())] impl PyCThunk { #[pygetset] fn callable(&self) -> PyObjectRef { diff --git a/crates/vm/src/stdlib/_functools.rs b/crates/vm/src/stdlib/_functools.rs index 6df6b2185..fbeb452dd 100644 --- a/crates/vm/src/stdlib/_functools.rs +++ b/crates/vm/src/stdlib/_functools.rs @@ -3,7 +3,7 @@ pub(crate) use _functools::module_def; #[pymodule] mod _functools { use crate::{ - Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, builtins::{ PyBoundMethod, PyDict, PyDictRef, PyGenericAlias, PyTuple, PyType, PyTypeRef, object, }, @@ -11,13 +11,13 @@ mod _functools { function::{ Either, FuncArgs, KwArgs, OptionalOption, PosArgs, PyComparisonValue, PySetterValue, }, - object::{AsObject, TraverseFn}, + object::{AsObject, PyAtomicRef, TraverseFn}, protocol::PyIter, pyclass, recursion::ReprGuard, types::{Callable, Constructor, GetDescriptor, PyComparisonOp, Representable}, }; - use core::sync::atomic::{AtomicU64, Ordering}; + use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use parking_lot::lock_api::RawReentrantMutex as GenericRawReentrantMutex; use rustpython_common::wtf8::Wtf8Buf; @@ -225,34 +225,27 @@ mod _functools { #[pyclass(name = "partial", module = "functools", traverse = "manual")] #[derive(Debug, PyPayload)] pub(super) struct PyPartial { - inner: PyRwLock, - } - - #[derive(Debug)] - struct PyPartialInner { - func: PyObjectRef, - args: PyRef, - keywords: PyRef, - phcount: usize, + #[pymember] + func: PyAtomicRef, + #[pymember] + args: PyAtomicRef, + #[pymember] + keywords: PyAtomicRef, + phcount: AtomicUsize, } // SAFETY: Each owned reference is visited once, without cloning it. unsafe impl crate::object::Traverse for PyPartial { fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) { - if let Some(inner) = self.inner.try_read_recursive() { - inner.func.traverse(tracer_fn); - inner.args.traverse(tracer_fn); - inner.keywords.traverse(tracer_fn); - } + self.func.traverse(tracer_fn); + self.args.traverse(tracer_fn); + self.keywords.traverse(tracer_fn); } fn clear(&mut self, out: &mut Vec) { // __setstate__ can make func point directly back to this partial. // The args tuple and keywords dict have their own cycle clearing. - out.push(core::mem::replace( - &mut self.inner.get_mut().func, - Context::genesis().none(), - )); + out.push(self.func.store(Context::genesis().none())); } } @@ -261,21 +254,6 @@ mod _functools { flags(BASETYPE, HAS_DICT, HAS_WEAKREF) )] impl PyPartial { - #[pygetset] - fn func(&self) -> PyObjectRef { - self.inner.read().func.clone() - } - - #[pygetset] - fn args(&self) -> PyRef { - self.inner.read().args.clone() - } - - #[pygetset] - fn keywords(&self) -> PyRef { - self.inner.read().keywords.clone() - } - #[pygetset] fn __dict__(zelf: &Py, vm: &VirtualMachine) -> PyDictRef { zelf.as_object() @@ -294,7 +272,9 @@ mod _functools { #[pymethod] fn __reduce__(zelf: &Py, vm: &VirtualMachine) -> PyObjectRef { - let inner = zelf.inner.read(); + let func = zelf.func.load_owned(); + let args = zelf.args.load_owned(); + let keywords = zelf.keywords.load_owned(); let partial_type = zelf.class(); // Get __dict__ if it exists and is not empty @@ -303,16 +283,13 @@ mod _functools { _ => vm.ctx.none(), }; - let state = vm.ctx.new_tuple(vec![ - inner.func.clone(), - inner.args.clone().into(), - inner.keywords.clone().into(), - dict_obj, - ]); + let state = + vm.ctx + .new_tuple(vec![func.clone(), args.into(), keywords.into(), dict_obj]); vm.ctx .new_tuple(vec![ partial_type.to_owned().into(), - vm.ctx.new_tuple(vec![inner.func.clone()]).into(), + vm.ctx.new_tuple(vec![func]).into(), state.into(), ]) .into() @@ -383,19 +360,10 @@ mod _functools { } let phcount = count_placeholders(args_slice); - // Actually update the state - let old_inner = core::mem::replace( - &mut *zelf.inner.write(), - PyPartialInner { - func: func.clone(), - // Handle args - use the already validated tuple - args: args_tuple, - // Handle keywords - keep the original type - keywords: keywords_dict, - phcount, - }, - ); - drop(old_inner); + zelf.func.store(func.clone()); + zelf.args.store(args_tuple); + zelf.keywords.store(keywords_dict); + zelf.phcount.store(phcount, Ordering::Relaxed); // Update __dict__ if provided let Some(instance_dict) = zelf.as_object().dict() else { @@ -489,8 +457,10 @@ mod _functools { // Handle nested partial objects let (final_func, final_args, final_keywords) = if let Some(partial) = func.downcast_ref::() { - let inner = partial.inner.read(); - let stored_args = inner.args.as_slice(); + let stored_args_obj = partial.args.load_owned(); + let stored_func = partial.func.load_owned(); + let stored_keywords = partial.keywords.load_owned(); + let stored_args = stored_args_obj.as_slice(); // Merge placeholders: replace placeholders in stored_args with new args let mut merged_args = Vec::with_capacity(stored_args.len() + args_slice.len()); @@ -511,7 +481,7 @@ mod _functools { // Append remaining new args merged_args.extend(new_args_iter.cloned()); - (inner.func.clone(), merged_args, inner.keywords.clone()) + (stored_func, merged_args, stored_keywords) } else { (func.clone(), args_slice.to_vec(), vm.ctx.new_dict()) }; @@ -529,12 +499,10 @@ mod _functools { } Ok(Self { - inner: PyRwLock::new(PyPartialInner { - func: final_func, - args: vm.ctx.new_tuple(final_args), - keywords: final_keywords, - phcount, - }), + func: PyAtomicRef::from(final_func), + args: PyAtomicRef::from(vm.ctx.new_tuple(final_args)), + keywords: PyAtomicRef::from(final_keywords), + phcount: AtomicUsize::new(phcount), }) } } @@ -544,15 +512,10 @@ mod _functools { fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { // Clone and release lock before calling Python code to prevent deadlock - let (func, stored_args, keywords, phcount) = { - let inner = zelf.inner.read(); - ( - inner.func.clone(), - inner.args.clone(), - inner.keywords.clone(), - inner.phcount, - ) - }; + let func = zelf.func.load_owned(); + let stored_args = zelf.args.load_owned(); + let keywords = zelf.keywords.load_owned(); + let phcount = zelf.phcount.load(Ordering::Relaxed); // Check if we have enough args to fill placeholders if phcount > 0 && args.args.len() < phcount { @@ -629,14 +592,9 @@ mod _functools { let obj = zelf.as_object(); if let Some(_guard) = ReprGuard::enter(vm, obj) { // Clone and release lock before calling Python code to prevent deadlock - let (func, args, keywords) = { - let inner = zelf.inner.read(); - ( - inner.func.clone(), - inner.args.clone(), - inner.keywords.clone(), - ) - }; + let func = zelf.func.load_owned(); + let args = zelf.args.load_owned(); + let keywords = zelf.keywords.load_owned(); let qualname = zelf.class().__qualname__(vm); let qualname_wtf8 = qualname @@ -645,7 +603,7 @@ mod _functools { || Wtf8Buf::from(zelf.class().name().to_owned()), |s| s.as_wtf8().to_owned(), ); - let module = zelf.class().__module__(vm); + let module = zelf.class().__module__(vm)?; let mut result = Wtf8Buf::new(); if let Ok(module_str) = module.downcast::() { diff --git a/crates/vm/src/stdlib/_io.rs b/crates/vm/src/stdlib/_io.rs index 7e264d17a..15d5b27a5 100644 --- a/crates/vm/src/stdlib/_io.rs +++ b/crates/vm/src/stdlib/_io.rs @@ -975,9 +975,10 @@ mod _io { } } + type RawCell = crate::object::PyAtomicRef>; + #[derive(Debug, Default)] struct BufferedData { - raw: Option, flags: BufferedFlags, abs_pos: Offset, buffer: Vec, @@ -989,19 +990,24 @@ mod _io { } impl BufferedData { - fn check_init(&self, vm: &VirtualMachine) -> PyResult<&PyObject> { - if let Some(raw) = &self.raw { - Ok(raw) + fn missing_raw_msg(&self) -> &'static str { + if self.flags.contains(BufferedFlags::DETACHED) { + "raw stream has been detached" } else { - let msg = if self.flags.contains(BufferedFlags::DETACHED) { - "raw stream has been detached" - } else { - "I/O operation on uninitialized object" - }; - Err(vm.new_value_error(msg)) + "I/O operation on uninitialized object" } } + fn check_init<'a>(&self, raw: &'a RawCell, vm: &VirtualMachine) -> PyResult<&'a PyObject> { + raw.deref() + .ok_or_else(|| vm.new_value_error(self.missing_raw_msg())) + } + + fn raw_owned(&self, raw: &RawCell, vm: &VirtualMachine) -> PyResult { + raw.load_owned() + .ok_or_else(|| vm.new_value_error(self.missing_raw_msg())) + } + #[inline] const fn writable(&self) -> bool { self.flags.contains(BufferedFlags::WRITABLE) @@ -1049,7 +1055,7 @@ mod _io { self.write_end = -1; } - fn flush(&mut self, vm: &VirtualMachine) -> PyResult<()> { + fn flush(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<()> { if !self.valid_write() || self.write_pos == self.write_end { self.reset_write(); return Ok(()); @@ -1057,13 +1063,17 @@ mod _io { let rewind = self.raw_offset() + (self.pos - self.write_pos); if rewind != 0 { - self.raw_seek(-rewind, 1, vm)?; + self.raw_seek(raw, -rewind, 1, vm)?; self.raw_pos -= rewind; } while self.write_pos < self.write_end { - let n = - self.raw_write(None, self.write_pos as usize..self.write_end as usize, vm)?; + let n = self.raw_write( + raw, + None, + self.write_pos as usize..self.write_end as usize, + vm, + )?; let n = match n { Some(n) => n, None => { @@ -1091,18 +1101,24 @@ mod _io { Ok(()) } - fn flush_rewind(&mut self, vm: &VirtualMachine) -> PyResult<()> { - self.flush(vm)?; + fn flush_rewind(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult<()> { + self.flush(raw, vm)?; if self.readable() { - let res = self.raw_seek(-self.raw_offset(), 1, vm); + let res = self.raw_seek(raw, -self.raw_offset(), 1, vm); self.reset_read(); res?; } Ok(()) } - fn raw_seek(&mut self, pos: Offset, whence: i32, vm: &VirtualMachine) -> PyResult { - let ret = vm.call_method(self.check_init(vm)?, "seek", (pos, whence))?; + fn raw_seek( + &mut self, + raw: &RawCell, + pos: Offset, + whence: i32, + vm: &VirtualMachine, + ) -> PyResult { + let ret = vm.call_method(self.check_init(raw, vm)?, "seek", (pos, whence))?; let offset = get_offset(&ret, vm)?; if offset < 0 { return Err( @@ -1113,9 +1129,15 @@ mod _io { Ok(offset) } - fn seek(&mut self, target: Offset, whence: i32, vm: &VirtualMachine) -> PyResult { + fn seek( + &mut self, + raw: &RawCell, + target: Offset, + whence: i32, + vm: &VirtualMachine, + ) -> PyResult { if matches!(whence, 0 | 1) && self.readable() { - let current = self.raw_tell_cache(vm)?; + let current = self.raw_tell_cache(raw, vm)?; let available = self.readahead(); if available > 0 { let offset = if whence == 0 { @@ -1134,14 +1156,14 @@ mod _io { } // raw.get_attr("seek", vm)?.call(args, vm) if self.writable() { - self.flush(vm)?; + self.flush(raw, vm)?; } let target = if whence == 1 { target - self.raw_offset() } else { target }; - let res = self.raw_seek(target, whence, vm); + let res = self.raw_seek(raw, target, whence, vm); self.raw_pos = -1; if res.is_ok() && self.readable() { self.reset_read(); @@ -1149,9 +1171,9 @@ mod _io { res } - fn raw_tell(&mut self, vm: &VirtualMachine) -> PyResult { - let raw = self.check_init(vm)?; - let ret = vm.call_method(raw, "tell", ())?; + fn raw_tell(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult { + let raw_obj = self.check_init(raw, vm)?; + let ret = vm.call_method(raw_obj, "tell", ())?; let offset = get_offset(&ret, vm)?; if offset < 0 { return Err( @@ -1162,9 +1184,9 @@ mod _io { Ok(offset) } - fn raw_tell_cache(&mut self, vm: &VirtualMachine) -> PyResult { + fn raw_tell_cache(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult { if self.abs_pos == -1 { - self.raw_tell(vm) + self.raw_tell(raw, vm) } else { Ok(self.abs_pos) } @@ -1173,6 +1195,7 @@ mod _io { /// None means non-blocking failed fn raw_write( &mut self, + raw: &RawCell, buf: Option, buf_range: Range, vm: &VirtualMachine, @@ -1196,7 +1219,7 @@ mod _io { // Loop if write() raises EINTR (PEP 475) let res = loop { - let res = vm.call_method(self.raw.as_ref().unwrap(), "write", (mem_obj.clone(),)); + let res = vm.call_method(self.check_init(raw, vm)?, "write", (mem_obj.clone(),)); match trap_eintr(res, vm) { Ok(Some(val)) => break Ok(val), Ok(None) => continue, @@ -1227,7 +1250,12 @@ mod _io { Ok(Some(n as usize)) } - fn write(&mut self, obj: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + fn write( + &mut self, + raw: &RawCell, + obj: ArgBytesLike, + vm: &VirtualMachine, + ) -> PyResult { if !self.valid_read() && !self.valid_write() { self.pos = 0; self.raw_pos = 0; @@ -1252,7 +1280,7 @@ mod _io { // if BlockingIOError, shift buffer // and try to buffer the new data; otherwise propagate the error - match self.flush(vm) { + match self.flush(raw, vm) { Ok(()) => {} Err(e) if e.fast_isinstance(vm.ctx.exceptions.blocking_io_error) => { if self.readable() { @@ -1300,7 +1328,7 @@ mod _io { // Only reach here if flush succeeded let offset = self.raw_offset(); if offset != 0 { - self.raw_seek(-offset, 1, vm)?; + self.raw_seek(raw, -offset, 1, vm)?; self.raw_pos -= offset; } @@ -1308,7 +1336,7 @@ mod _io { let mut written = 0; let buffer: PyBuffer = obj.into(); while remaining > self.buffer.len() { - let res = self.raw_write(Some(buffer.clone()), written..buf_len, vm)?; + let res = self.raw_write(raw, Some(buffer.clone()), written..buf_len, vm)?; match res { Some(n) => { written += n; @@ -1375,7 +1403,12 @@ mod _io { Some(ret) } - fn read_generic(&mut self, n: usize, vm: &VirtualMachine) -> PyResult>> { + fn read_generic( + &mut self, + raw: &RawCell, + n: usize, + vm: &VirtualMachine, + ) -> PyResult>> { if let Some(fast) = self.read_fast(n) { return Ok(Some(fast)); } @@ -1393,7 +1426,7 @@ mod _io { self.pos += current_size as Offset; } if self.writable() { - self.flush_rewind(vm)?; + self.flush_rewind(raw, vm)?; } self.reset_read(); macro_rules! handle_opt_read { @@ -1414,7 +1447,7 @@ mod _io { if r == 0 { break; } - let r = self.raw_read(Either::A(Some(&mut out)), written..written + r, vm)?; + let r = self.raw_read(raw, Either::A(Some(&mut out)), written..written + r, vm)?; let r = handle_opt_read!(r); remaining -= r; written += r; @@ -1424,7 +1457,7 @@ mod _io { self.read_end = 0; while remaining > 0 && (self.read_end as usize) < self.buffer.len() { - let r = handle_opt_read!(self.fill_buffer(vm)?); + let r = handle_opt_read!(self.fill_buffer(raw, vm)?); if remaining > r { out[written..][..r].copy_from_slice(&self.buffer[self.pos as usize..][..r]); written += r; @@ -1445,14 +1478,14 @@ mod _io { Ok(Some(out)) } - fn fill_buffer(&mut self, vm: &VirtualMachine) -> PyResult> { + fn fill_buffer(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult> { let start = if self.valid_read() { self.read_end as usize } else { 0 }; let buf_end = self.buffer.len(); - let res = self.raw_read(Either::A(None), start..buf_end, vm)?; + let res = self.raw_read(raw, Either::A(None), start..buf_end, vm)?; if let Some(n) = res.filter(|n| *n > 0) { let new_start = (start + n) as Offset; self.read_end = new_start; @@ -1463,11 +1496,13 @@ mod _io { fn raw_read( &mut self, + raw: &RawCell, v: Either>, PyBuffer>, buf_range: Range, vm: &VirtualMachine, ) -> PyResult> { let len = buf_range.len(); + let raw_obj = self.check_init(raw, vm)?; let res = match v { Either::A(v) => { let v = v.unwrap_or(&mut self.buffer); @@ -1481,11 +1516,7 @@ mod _io { // Loop if readinto() raises EINTR (PEP 475) let res = loop { - let res = vm.call_method( - self.raw.as_ref().unwrap(), - "readinto", - (mem_obj.clone(),), - ); + let res = vm.call_method(raw_obj, "readinto", (mem_obj.clone(),)); match trap_eintr(res, vm) { Ok(Some(val)) => break Ok(val), Ok(None) => continue, // EINTR, retry @@ -1504,11 +1535,7 @@ mod _io { PyMemoryView::from_buffer_range(buf, buf_range, vm)?.into_ref(&vm.ctx); // Loop if readinto() raises EINTR (PEP 475) loop { - let res = vm.call_method( - self.raw.as_ref().unwrap(), - "readinto", - (mem_obj.clone(),), - ); + let res = vm.call_method(raw_obj, "readinto", (mem_obj.clone(),)); match trap_eintr(res, vm)? { Some(val) => break val, None => continue, @@ -1540,7 +1567,7 @@ mod _io { Ok(Some(n as usize)) } - fn read_all(&mut self, vm: &VirtualMachine) -> PyResult> { + fn read_all(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult> { let buf = self.active_read_slice(); let data = if buf.is_empty() { None @@ -1551,11 +1578,11 @@ mod _io { }; if self.writable() { - self.flush_rewind(vm)?; + self.flush_rewind(raw, vm)?; } let readall = vm - .get_str_method(self.raw.clone().unwrap(), "readall") + .get_str_method(self.raw_owned(raw, vm)?, "readall") .transpose()?; if let Some(readall) = readall { let res = readall.call((), vm)?; @@ -1577,7 +1604,7 @@ mod _io { loop { // Loop with EINTR handling (PEP 475) let read_data = loop { - let res = vm.call_method(self.raw.as_ref().unwrap(), "read", ()); + let res = vm.call_method(self.check_init(raw, vm)?, "read", ()); match trap_eintr(res, vm)? { Some(val) => break val, None => continue, @@ -1618,13 +1645,13 @@ mod _io { } } - fn peek(&mut self, vm: &VirtualMachine) -> PyResult> { + fn peek(&mut self, raw: &RawCell, vm: &VirtualMachine) -> PyResult> { let have = self.readahead(); let slice = if have > 0 { &self.buffer[self.pos as usize..][..have as usize] } else { self.reset_read(); - let r = self.fill_buffer(vm)?.unwrap_or(0); + let r = self.fill_buffer(raw, vm)?.unwrap_or(0); self.pos = 0; &self.buffer[..r] }; @@ -1633,6 +1660,7 @@ mod _io { fn readinto_generic( &mut self, + raw: &RawCell, buf: PyBuffer, readinto1: bool, vm: &VirtualMachine, @@ -1656,7 +1684,7 @@ mod _io { } } if self.writable() { - self.flush_rewind(vm)?; + self.flush_rewind(raw, vm)?; } self.reset_read(); self.pos = 0; @@ -1664,9 +1692,14 @@ mod _io { let mut remaining = buf_len - written; while remaining > 0 { let n = if remaining > self.buffer.len() { - self.raw_read(Either::B(buf.clone()), written..written + remaining, vm)? + self.raw_read( + raw, + Either::B(buf.clone()), + written..written + remaining, + vm, + )? } else if !(readinto1 && written != 0) { - let n = self.fill_buffer(vm)?; + let n = self.fill_buffer(raw, vm)?; if let Some(n) = n.filter(|&n| n > 0) { let n = core::cmp::min(n, remaining); buf.as_contiguous_mut().unwrap()[written..][..n] @@ -1746,6 +1779,7 @@ mod _io { const SEEKABLE: bool = false; fn data(&self) -> &PyThreadMutex; + fn raw_cell(&self) -> &RawCell; fn closing(&self) -> &AtomicBool; fn finalizing(&self) -> &AtomicBool; @@ -1774,7 +1808,7 @@ mod _io { vm: &VirtualMachine, ) -> PyResult<()> { let mut data = self.lock(vm)?; - data.raw = None; + self.raw_cell().store(None); data.flags.remove(BufferedFlags::DETACHED); if buffer_size <= 0 { @@ -1806,7 +1840,7 @@ mod _io { data.pos = 0; } - data.raw = Some(raw); + self.raw_cell().store(Some(raw)); Ok(()) } @@ -1823,17 +1857,17 @@ mod _io { return Err(vm.new_value_error(format!("whence value {whence} unsupported"))); } let mut data = self.lock(vm)?; - let raw = data.check_init(vm)?; - ensure_unclosed(raw, "seek of closed file", vm)?; - check_seekable(raw, vm)?; + let raw_obj = data.check_init(self.raw_cell(), vm)?; + ensure_unclosed(raw_obj, "seek of closed file", vm)?; + check_seekable(raw_obj, vm)?; let target = get_offset(&target, vm)?; - data.seek(target, whence, vm) + data.seek(self.raw_cell(), target, whence, vm) } #[pymethod] fn tell(&self, vm: &VirtualMachine) -> PyResult { let mut data = self.lock(vm)?; - let raw_tell = data.raw_tell(vm)?; + let raw_tell = data.raw_tell(self.raw_cell(), vm)?; let raw_offset = data.raw_offset(); let mut pos = raw_tell - raw_offset; // GH-95782 @@ -1847,13 +1881,13 @@ mod _io { fn truncate(zelf: PyRef, pos: ObjPos, vm: &VirtualMachine) -> PyResult { let pos = pos.pos.unwrap_or_else(|| vm.ctx.none()); let mut data = zelf.lock(vm)?; - data.check_init(vm)?; + let raw_obj = data.check_init(zelf.raw_cell(), vm)?; if !data.writable() { return Err(new_unsupported_operation("truncate", vm)); } - data.flush_rewind(vm)?; - let res = vm.call_method(data.raw.as_ref().unwrap(), "truncate", (pos,))?; - let _ = data.raw_tell(vm); + data.flush_rewind(zelf.raw_cell(), vm)?; + let res = vm.call_method(raw_obj, "truncate", (pos,))?; + let _ = data.raw_tell(zelf.raw_cell(), vm); Ok(res) } #[pymethod] @@ -1861,25 +1895,21 @@ mod _io { vm.call_method(zelf.as_object(), "flush", ())?; let mut data = zelf.lock(vm)?; data.flags.insert(BufferedFlags::DETACHED); - data.raw - .take() + zelf.raw_cell() + .store(None) .ok_or_else(|| vm.new_value_error("raw stream has been detached")) } #[pymethod] fn seekable(&self, vm: &VirtualMachine) -> PyResult { - vm.call_method(self.lock(vm)?.check_init(vm)?, "seekable", ()) - } - - #[pygetset] - fn raw(&self, vm: &VirtualMachine) -> PyResult> { - Ok(self.lock(vm)?.raw.clone()) + let data = self.lock(vm)?; + vm.call_method(data.check_init(self.raw_cell(), vm)?, "seekable", ()) } /// Get raw stream without holding the lock (for calling Python code safely) fn get_raw_unlocked(&self, vm: &VirtualMachine) -> PyResult { let data = self.lock(vm)?; - Ok(data.check_init(vm)?.to_owned()) + data.raw_owned(self.raw_cell(), vm) } #[pygetset] @@ -1899,12 +1929,14 @@ mod _io { #[pymethod] fn fileno(&self, vm: &VirtualMachine) -> PyResult { - vm.call_method(self.lock(vm)?.check_init(vm)?, "fileno", ()) + let data = self.lock(vm)?; + vm.call_method(data.check_init(self.raw_cell(), vm)?, "fileno", ()) } #[pymethod] fn isatty(&self, vm: &VirtualMachine) -> PyResult { - vm.call_method(self.lock(vm)?.check_init(vm)?, "isatty", ()) + let data = self.lock(vm)?; + vm.call_method(data.check_init(self.raw_cell(), vm)?, "isatty", ()) } #[pyslot] @@ -1927,12 +1959,12 @@ mod _io { fn close_strict(&self, vm: &VirtualMachine) -> PyResult { let mut data = self.lock(vm)?; - let raw = data.check_init(vm)?; + let raw = data.check_init(self.raw_cell(), vm)?; if file_closed(raw, vm)? { return Ok(vm.ctx.none()); } - let flush_res = data.flush(vm); - let close_res = vm.call_method(data.raw.as_ref().unwrap(), "close", ()); + let flush_res = data.flush(self.raw_cell(), vm); + let close_res = vm.call_method(raw, "close", ()); exception_chain(flush_res, close_res) } @@ -1941,11 +1973,11 @@ mod _io { // Don't hold the lock while calling Python code to avoid reentrant lock issues let raw = { let data = zelf.lock(vm)?; - let raw = data.check_init(vm)?; - if file_closed(raw, vm)? { + let raw = data.raw_owned(zelf.raw_cell(), vm)?; + if file_closed(&raw, vm)? { return Ok(vm.ctx.none()); } - raw.to_owned() + raw }; if zelf.finalizing().load(Ordering::Relaxed) { // _dealloc_warn: delegate to raw._dealloc_warn(source) @@ -1992,8 +2024,8 @@ mod _io { ) -> PyResult<()> { // Get raw reference and release lock before calling downstream let raw = { - let data = zelf.lock(vm)?; - data.raw.clone() + let _data = zelf.lock(vm)?; + zelf.raw_cell().load_owned() }; if let Some(raw) = raw { let _ = vm.call_method(&raw, "_dealloc_warn", (source,)); @@ -2011,7 +2043,8 @@ mod _io { #[pymethod] fn read(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult> { let mut data = self.reader().lock(vm)?; - let raw = data.check_init(vm)?; + let raw_cell = self.reader().raw_cell(); + let raw = data.check_init(raw_cell, vm)?; let n = size.size.unwrap_or(-1); if n < -1 { return Err(vm.new_value_error("read length must be non-negative or -1")); @@ -2019,28 +2052,30 @@ mod _io { ensure_unclosed(raw, "read of closed file", vm)?; match n.to_usize() { Some(n) => data - .read_generic(n, vm) + .read_generic(raw_cell, n, vm) .map(|x| x.map(|b| PyBytes::from(b).into_ref(&vm.ctx))), - None => data.read_all(vm), + None => data.read_all(raw_cell, vm), } } #[pymethod] fn peek(&self, _size: PeekSize, vm: &VirtualMachine) -> PyResult> { let mut data = self.reader().lock(vm)?; - let raw = data.check_init(vm)?; + let raw_cell = self.reader().raw_cell(); + let raw = data.check_init(raw_cell, vm)?; ensure_unclosed(raw, "peek of closed file", vm)?; if data.writable() { - let _ = data.flush_rewind(vm); + let _ = data.flush_rewind(raw_cell, vm); } - data.peek(vm) + data.peek(raw_cell, vm) } #[pymethod] fn read1(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult> { let mut data = self.reader().lock(vm)?; - let raw = data.check_init(vm)?; + let raw_cell = self.reader().raw_cell(); + let raw = data.check_init(raw_cell, vm)?; ensure_unclosed(raw, "read of closed file", vm)?; let n = size.to_usize().unwrap_or(data.buffer.len()); if n == 0 { @@ -2053,12 +2088,12 @@ mod _io { } // Flush write buffer before reading if data.writable() { - data.flush_rewind(vm)?; + data.flush_rewind(raw_cell, vm)?; } let mut v = vm.new_zeroed_bytes(n)?; data.reset_read(); let r = data - .raw_read(Either::A(Some(&mut v)), 0..n, vm)? + .raw_read(raw_cell, Either::A(Some(&mut v)), 0..n, vm)? .unwrap_or(0); v.truncate(r); v.shrink_to_fit(); @@ -2072,9 +2107,10 @@ mod _io { vm: &VirtualMachine, ) -> PyResult> { let mut data = self.reader().lock(vm)?; - let raw = data.check_init(vm)?; + let raw_cell = self.reader().raw_cell(); + let raw = data.check_init(raw_cell, vm)?; ensure_unclosed(raw, "readinto of closed file", vm)?; - data.readinto_generic(buffer.into(), false, vm) + data.readinto_generic(raw_cell, buffer.into(), false, vm) } #[pymethod] @@ -2084,9 +2120,10 @@ mod _io { vm: &VirtualMachine, ) -> PyResult> { let mut data = self.reader().lock(vm)?; - let raw = data.check_init(vm)?; + let raw_cell = self.reader().raw_cell(); + let raw = data.check_init(raw_cell, vm)?; ensure_unclosed(raw, "readinto of closed file", vm)?; - data.readinto_generic(buffer.into(), true, vm) + data.readinto_generic(raw_cell, buffer.into(), true, vm) } #[pymethod] @@ -2095,7 +2132,7 @@ mod _io { // Don't hold the lock while calling Python code to avoid reentrant lock issues let raw = { let data = self.reader().lock(vm)?; - data.check_init(vm)?.to_owned() + data.raw_owned(self.reader().raw_cell(), vm)? }; ensure_unclosed(&raw, "flush of closed file", vm)?; vm.call_method(&raw, "flush", ())?; @@ -2116,14 +2153,28 @@ mod _io { #[pyattr] #[pyclass(name = "BufferedReader", base = _BufferedIOBase)] - #[derive(Debug, Default)] + #[derive(Debug)] struct BufferedReader { _base: _BufferedIOBase, + #[pymember] + raw: RawCell, data: PyThreadMutex, closing: AtomicBool, finalizing: AtomicBool, } + impl Default for BufferedReader { + fn default() -> Self { + Self { + _base: _BufferedIOBase::default(), + raw: RawCell::new_empty(), + data: PyThreadMutex::default(), + closing: AtomicBool::new(false), + finalizing: AtomicBool::new(false), + } + } + } + impl BufferedMixin for BufferedReader { const CLASS_NAME: &'static str = "BufferedReader"; const READABLE: bool = true; @@ -2133,6 +2184,10 @@ mod _io { &self.data } + fn raw_cell(&self) -> &RawCell { + &self.raw + } + fn closing(&self) -> &AtomicBool { &self.closing } @@ -2202,9 +2257,9 @@ mod _io { if self.writer().closing().load(Ordering::Acquire) { loop { let raw = { - let data = self.writer().lock(vm)?; - match &data.raw { - Some(raw) => raw.to_owned(), + let _data = self.writer().lock(vm)?; + match self.writer().raw_cell().load_owned() { + Some(raw) => raw, None => break, // detached } }; @@ -2217,31 +2272,47 @@ mod _io { return Err(vm.new_value_error("write to closed file")); } let mut data = self.writer().lock(vm)?; - let raw = data.check_init(vm)?; + let raw_cell = self.writer().raw_cell(); + let raw = data.check_init(raw_cell, vm)?; ensure_unclosed(raw, "write to closed file", vm)?; - data.write(buffer, vm) + data.write(raw_cell, buffer, vm) } #[pymethod] fn flush(&self, vm: &VirtualMachine) -> PyResult<()> { let mut data = self.writer().lock(vm)?; - let raw = data.check_init(vm)?; + let raw_cell = self.writer().raw_cell(); + let raw = data.check_init(raw_cell, vm)?; ensure_unclosed(raw, "flush of closed file", vm)?; - data.flush_rewind(vm) + data.flush_rewind(raw_cell, vm) } } #[pyattr] #[pyclass(name = "BufferedWriter", base = _BufferedIOBase)] - #[derive(Debug, Default)] + #[derive(Debug)] struct BufferedWriter { _base: _BufferedIOBase, + #[pymember] + raw: RawCell, data: PyThreadMutex, closing: AtomicBool, finalizing: AtomicBool, } + impl Default for BufferedWriter { + fn default() -> Self { + Self { + _base: _BufferedIOBase::default(), + raw: RawCell::new_empty(), + data: PyThreadMutex::default(), + closing: AtomicBool::new(false), + finalizing: AtomicBool::new(false), + } + } + } + impl BufferedMixin for BufferedWriter { const CLASS_NAME: &'static str = "BufferedWriter"; const READABLE: bool = false; @@ -2251,6 +2322,10 @@ mod _io { &self.data } + fn raw_cell(&self) -> &RawCell { + &self.raw + } + fn closing(&self) -> &AtomicBool { &self.closing } @@ -2307,14 +2382,28 @@ mod _io { #[pyattr] #[pyclass(name = "BufferedRandom", base = _BufferedIOBase)] - #[derive(Debug, Default)] + #[derive(Debug)] struct BufferedRandom { _base: _BufferedIOBase, + #[pymember] + raw: RawCell, data: PyThreadMutex, closing: AtomicBool, finalizing: AtomicBool, } + impl Default for BufferedRandom { + fn default() -> Self { + Self { + _base: _BufferedIOBase::default(), + raw: RawCell::new_empty(), + data: PyThreadMutex::default(), + closing: AtomicBool::new(false), + finalizing: AtomicBool::new(false), + } + } + } + impl BufferedMixin for BufferedRandom { const CLASS_NAME: &'static str = "BufferedRandom"; const READABLE: bool = true; @@ -2325,6 +2414,10 @@ mod _io { &self.data } + fn raw_cell(&self) -> &RawCell { + &self.raw + } + fn closing(&self) -> &AtomicBool { &self.closing } @@ -2703,14 +2796,10 @@ mod _io { #[derive(Debug)] struct TextIOData { - buffer: PyObjectRef, encoder: Option<(PyObjectRef, Option)>, decoder: Option, - encoding: PyUtf8StrRef, errors: PyUtf8StrRef, newline: Newlines, - line_buffering: bool, - write_through: bool, chunk_size: usize, seekable: bool, has_read1: bool, @@ -2955,13 +3044,35 @@ mod _io { #[pyattr] #[pyclass(name = "TextIOWrapper", base = _TextIOBase)] - #[derive(Debug, Default)] + #[derive(Debug)] struct TextIOWrapper { _base: _TextIOBase, + #[pymember] + buffer: RawCell, + #[pymember] + encoding: RawCell, + #[pymember] + line_buffering: AtomicBool, + #[pymember] + write_through: AtomicBool, data: PyThreadMutex>, finalizing: AtomicBool, } + impl Default for TextIOWrapper { + fn default() -> Self { + Self { + _base: _TextIOBase::default(), + buffer: RawCell::new_empty(), + encoding: RawCell::new_empty(), + line_buffering: AtomicBool::new(false), + write_through: AtomicBool::new(false), + data: PyThreadMutex::default(), + finalizing: AtomicBool::new(false), + } + } + } + impl DefaultConstructor for TextIOWrapper {} #[derive(FromArgs)] @@ -2980,6 +3091,10 @@ mod _io { TextIOWrapperInitArgs { buffer, args }: Self::Args, vm: &VirtualMachine, ) -> PyResult<()> { + zelf.buffer.store(None); + zelf.encoding.store(None); + zelf.line_buffering.store(false, Ordering::Relaxed); + zelf.write_through.store(false, Ordering::Relaxed); let mut data = zelf.lock_opt(vm)?; *data = None; @@ -3007,15 +3122,16 @@ mod _io { None => false, }; + zelf.buffer.store(Some(buffer)); + zelf.encoding.store(Some(encoding.into())); + zelf.line_buffering.store(line_buffering, Ordering::Relaxed); + zelf.write_through.store(write_through, Ordering::Relaxed); + *data = Some(TextIOData { - buffer, encoder, decoder, - encoding, errors, newline, - line_buffering, - write_through, chunk_size: 8192, seekable, has_read1, @@ -3034,6 +3150,10 @@ mod _io { fn slot_init(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { let zelf_ref: &Py = zelf.try_to_ref(vm)?; { + zelf_ref.buffer.store(None); + zelf_ref.encoding.store(None); + zelf_ref.line_buffering.store(false, Ordering::Relaxed); + zelf_ref.write_through.store(false, Ordering::Relaxed); let mut data = zelf_ref.lock_opt(vm)?; *data = None; } @@ -3235,11 +3355,28 @@ mod _io { } #[inline] - fn flush_inner(textio: &mut TextIOData, vm: &VirtualMachine) -> PyResult { - textio.check_closed(vm)?; + fn flush_inner(textio: &mut TextIOData, buffer: &PyObject, vm: &VirtualMachine) -> PyResult { + textio.check_closed(buffer, vm)?; textio.telling = textio.seekable; - textio.write_pending(vm)?; - vm.call_method(&textio.buffer, "flush", ()) + textio.write_pending(buffer, vm)?; + vm.call_method(buffer, "flush", ()) + } + + fn text_buffer(cell: &RawCell, vm: &VirtualMachine) -> PyResult { + cell.load_owned() + .ok_or_else(|| vm.new_value_error("underlying buffer has been detached")) + } + + fn textio_encoding(cell: &RawCell, vm: &VirtualMachine) -> PyResult { + let obj = cell + .load_owned() + .ok_or_else(|| vm.new_value_error("I/O operation on uninitialized object"))?; + obj.downcast::().map_err(|obj| { + vm.new_type_error(format!( + "encoding must be a str, not '{}'", + obj.class().name() + )) + }) } #[pyclass( @@ -3261,9 +3398,10 @@ mod _io { vm: &VirtualMachine, ) -> PyResult<()> { let mut data = self.lock(vm)?; - data.check_closed(vm)?; + let buffer = text_buffer(&self.buffer, vm)?; + data.check_closed(&buffer, vm)?; - let mut encoding = data.encoding.clone(); + let mut encoding = textio_encoding(&self.encoding, vm)?; let mut errors = data.errors.clone(); let mut newline = data.newline; let mut encoding_changed = false; @@ -3317,17 +3455,17 @@ mod _io { } if data.pending.num_bytes > 0 { - data.write_pending(vm)?; + data.write_pending(&buffer, vm)?; } - vm.call_method(&data.buffer, "flush", ())?; + vm.call_method(&buffer, "flush", ())?; if encoding_changed || errors_changed || newline_changed { if data.pending.num_bytes > 0 { - data.write_pending(vm)?; + data.write_pending(&buffer, vm)?; } let (encoder, decoder) = - Self::find_coder(&data.buffer, encoding.as_str(), &errors, newline, vm)?; - data.encoding = encoding; + Self::find_coder(&buffer, encoding.as_str(), &errors, newline, vm)?; + self.encoding.store(Some(encoding.clone().into())); data.errors = errors; data.newline = newline; data.encoder = encoder; @@ -3336,20 +3474,15 @@ mod _io { data.snapshot = None; data.decoded_chars_used = Utf8size::default(); if let Some((encoder, _)) = &data.encoder { - Self::adjust_encoder_state_for_bom( - encoder, - data.encoding.as_str(), - &data.buffer, - vm, - )?; + Self::adjust_encoder_state_for_bom(encoder, encoding.as_str(), &buffer, vm)?; } } if let Some(line_buffering) = line_buffering { - data.line_buffering = line_buffering; + self.line_buffering.store(line_buffering, Ordering::Relaxed); } if let Some(write_through) = write_through { - data.write_through = write_through; + self.write_through.store(write_through, Ordering::Relaxed); } Ok(()) } @@ -3357,45 +3490,34 @@ mod _io { #[pymethod] fn detach(zelf: PyRef, vm: &VirtualMachine) -> PyResult { let mut textio = zelf.lock(vm)?; - - // Fail fast if already detached - if vm.is_none(&textio.buffer) { - return Err(vm.new_value_error("underlying buffer has been detached")); - } - - flush_inner(&mut textio, vm)?; - - let buffer = textio.buffer.clone(); - textio.buffer = vm.ctx.none(); + let buffer = text_buffer(&zelf.buffer, vm)?; + flush_inner(&mut textio, &buffer, vm)?; + zelf.buffer.store(None); Ok(buffer) } #[pymethod] fn seekable(&self, vm: &VirtualMachine) -> PyResult { let textio = self.lock(vm)?; - vm.call_method(&textio.buffer, "seekable", ()) + let buffer = text_buffer(&self.buffer, vm)?; + drop(textio); + vm.call_method(&buffer, "seekable", ()) } #[pymethod] fn readable(&self, vm: &VirtualMachine) -> PyResult { let textio = self.lock(vm)?; - vm.call_method(&textio.buffer, "readable", ()) + let buffer = text_buffer(&self.buffer, vm)?; + drop(textio); + vm.call_method(&buffer, "readable", ()) } #[pymethod] fn writable(&self, vm: &VirtualMachine) -> PyResult { let textio = self.lock(vm)?; - vm.call_method(&textio.buffer, "writable", ()) - } - - #[pygetset] - fn line_buffering(&self, vm: &VirtualMachine) -> PyResult { - Ok(self.lock(vm)?.line_buffering) - } - - #[pygetset] - fn write_through(&self, vm: &VirtualMachine) -> PyResult { - Ok(self.lock(vm)?.write_through) + let buffer = text_buffer(&self.buffer, vm)?; + drop(textio); + vm.call_method(&buffer, "writable", ()) } #[pygetset] @@ -3416,9 +3538,10 @@ mod _io { fn set_chunksize(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { { let textio = self.lock(vm)?; - if vm.is_none(&textio.buffer) { + if self.buffer.deref().is_none() { return Err(vm.new_value_error("underlying buffer has been detached")); } + drop(textio); } let chunk_size: isize = match value { @@ -3487,7 +3610,8 @@ mod _io { // SEEK_CUR 1 => { if vm.bool_eq(&cookie, vm.ctx.new_int(0).as_ref())? { - vm.call_method(&textio.buffer, "tell", ())? + let buffer = text_buffer(&zelf.buffer, vm)?; + vm.call_method(&buffer, "tell", ())? } else { return Err(new_unsupported_operation( "can't do nonzero cur-relative seeks", @@ -3506,7 +3630,8 @@ mod _io { if let Some(decoder) = &textio.decoder { vm.call_method(decoder, "reset", ())?; } - let res = vm.call_method(&textio.buffer, "seek", (0, 2))?; + let buffer = text_buffer(&zelf.buffer, vm)?; + let res = vm.call_method(&buffer, "seek", (0, 2))?; if let Some((encoder, _)) = &textio.encoder { let start_of_stream = vm.bool_eq(&res, vm.ctx.new_int(0).as_ref())?; reset_encoder(encoder, start_of_stream)?; @@ -3536,31 +3661,27 @@ mod _io { let cookie = TextIOCookie::parse(cookie_obj.as_bigint()) .ok_or_else(|| vm.new_value_error("invalid cookie"))?; let mut textio = zelf.lock(vm)?; - vm.call_method(&textio.buffer, "seek", (cookie.start_pos,))?; + let buffer = text_buffer(&zelf.buffer, vm)?; + vm.call_method(&buffer, "seek", (cookie.start_pos,))?; textio.set_decoded_chars(None); textio.snapshot = None; if let Some(decoder) = &textio.decoder { cookie.set_decoder_state(decoder, vm)?; } if cookie.chars_to_skip != 0 { - let TextIOData { - ref decoder, - ref buffer, - ref mut snapshot, - .. - } = *textio; - let decoder = decoder - .as_ref() + let decoder = textio + .decoder + .clone() .ok_or_else(|| vm.new_value_error("invalid cookie"))?; - let input_chunk = vm.call_method(buffer, "read", (cookie.bytes_to_feed,))?; + let input_chunk = vm.call_method(&buffer, "read", (cookie.bytes_to_feed,))?; let input_chunk: PyBytesRef = input_chunk.downcast().map_err(|obj| { vm.new_type_error(format!( "underlying read() should have returned a bytes object, not '{}'", obj.class().name() )) })?; - *snapshot = Some((cookie.dec_flags, input_chunk.clone())); - let decoded = vm.call_method(decoder, "decode", (input_chunk, cookie.need_eof))?; + textio.snapshot = Some((cookie.dec_flags, input_chunk.clone())); + let decoded = vm.call_method(&decoder, "decode", (input_chunk, cookie.need_eof))?; let decoded = check_decoded(decoded, vm)?; // The position is stored both as a count of characters and as // an offset in bytes, so both have to land inside what was @@ -3595,11 +3716,13 @@ mod _io { if !textio.telling { return Err(vm.new_os_error("telling position disabled by next() call")); } - textio.write_pending(vm)?; + let buffer = text_buffer(&zelf.buffer, vm)?; + textio.write_pending(&buffer, vm)?; drop(textio); vm.call_method(zelf.as_object(), "flush", ())?; let textio = zelf.lock(vm)?; - let pos = vm.call_method(&textio.buffer, "tell", ())?; + let buffer = text_buffer(&zelf.buffer, vm)?; + let pos = vm.call_method(&buffer, "tell", ())?; let (decoder, (dec_flags, next_input)) = match (&textio.decoder, &textio.snapshot) { (Some(d), Some(s)) => (d, s), _ => return Ok(pos), @@ -3693,13 +3816,8 @@ mod _io { #[pygetset] fn name(&self, vm: &VirtualMachine) -> PyResult { - let buffer = self.lock(vm)?.buffer.clone(); - buffer.get_attr("name", vm) - } - - #[pygetset] - fn encoding(&self, vm: &VirtualMachine) -> PyResult { - Ok(self.lock(vm)?.encoding.clone()) + let _textio = self.lock(vm)?; + text_buffer(&self.buffer, vm)?.get_attr("name", vm) } #[pygetset] @@ -3709,20 +3827,22 @@ mod _io { #[pymethod] fn fileno(&self, vm: &VirtualMachine) -> PyResult { - let buffer = self.lock(vm)?.buffer.clone(); + let _textio = self.lock(vm)?; + let buffer = text_buffer(&self.buffer, vm)?; vm.call_method(&buffer, "fileno", ()) } #[pymethod] fn read(&self, size: OptionalSize, vm: &VirtualMachine) -> PyResult { let mut textio = self.lock(vm)?; - textio.check_closed(vm)?; + let buffer = text_buffer(&self.buffer, vm)?; + textio.check_closed(&buffer, vm)?; let decoder = textio .decoder .clone() .ok_or_else(|| new_unsupported_operation("not readable", vm))?; - textio.write_pending(vm)?; + textio.write_pending(&buffer, vm)?; let s = if let Some(mut remaining) = size.to_usize() { let mut chunks = Vec::new(); @@ -3736,7 +3856,7 @@ mod _io { if remaining == 0 { break; } - let eof = textio.read_chunk(remaining, vm)?; + let eof = textio.read_chunk(&buffer, remaining, vm)?; if eof { break; } @@ -3753,7 +3873,7 @@ mod _io { PyStr::from(ret).into_ref(&vm.ctx) } } else { - let bytes = vm.call_method(&textio.buffer, "read", ())?; + let bytes = vm.call_method(&buffer, "read", ())?; let decoded = vm.call_method(&decoder, "decode", (bytes, true))?; let decoded = check_decoded(decoded, vm)?; let ret = textio.take_decoded_chars(Some(decoded), vm); @@ -3766,7 +3886,8 @@ mod _io { #[pymethod] fn write(&self, text: PyStrRef, vm: &VirtualMachine) -> PyResult { let mut textio = self.lock(vm)?; - textio.check_closed(vm)?; + let buffer = text_buffer(&self.buffer, vm)?; + textio.check_closed(&buffer, vm)?; let (encoder, encode_func) = textio .encoder @@ -3784,9 +3905,10 @@ mod _io { Newlines::Universal if cfg!(windows) => Some("\r\n"), _ => None, }; - let has_lf = (replace_nl.is_some() || textio.line_buffering) - && data.contains_code_point('\n'.into()); - let flush = textio.line_buffering && (has_lf || data.contains_code_point('\r'.into())); + let line_buffering = self.line_buffering.load(Ordering::Relaxed); + let has_lf = + (replace_nl.is_some() || line_buffering) && data.contains_code_point('\n'.into()); + let flush = line_buffering && (has_lf || data.contains_code_point('\r'.into())); let chunk = if let Some(replace_nl) = replace_nl { if has_lf { PyStr::from(data.replace("\n".as_ref(), replace_nl.as_ref())).into_ref(&vm.ctx) @@ -3818,34 +3940,38 @@ mod _io { if textio.pending.num_bytes > 0 && textio.pending.num_bytes + chunk.as_bytes().len() > textio.chunk_size { - let buffer = textio.buffer.clone(); let pending = textio.pending.take(vm); drop(textio); vm.call_method(&buffer, "write", (pending,))?; textio = self.lock(vm)?; - textio.check_closed(vm)?; + let buffer = text_buffer(&self.buffer, vm)?; + textio.check_closed(&buffer, vm)?; if textio.pending.num_bytes > 0 { - let buffer = textio.buffer.clone(); let pending = textio.pending.take(vm); drop(textio); vm.call_method(&buffer, "write", (pending,))?; textio = self.lock(vm)?; - textio.check_closed(vm)?; + let buffer = text_buffer(&self.buffer, vm)?; + textio.check_closed(&buffer, vm)?; } } textio.pending.push(chunk); if textio.pending.num_bytes > 0 - && (flush || textio.write_through || textio.pending.num_bytes >= textio.chunk_size) + && (flush + || self.write_through.load(Ordering::Relaxed) + || textio.pending.num_bytes >= textio.chunk_size) { - let buffer = textio.buffer.clone(); + let buffer = text_buffer(&self.buffer, vm)?; let pending = textio.pending.take(vm); drop(textio); vm.call_method(&buffer, "write", (pending,))?; textio = self.lock(vm)?; - textio.check_closed(vm)?; + let buffer = text_buffer(&self.buffer, vm)?; + textio.check_closed(&buffer, vm)?; } if flush { - let _ = vm.call_method(&textio.buffer, "flush", ()); + let buffer = text_buffer(&self.buffer, vm)?; + let _ = vm.call_method(&buffer, "flush", ()); } Ok(char_len) @@ -3854,15 +3980,16 @@ mod _io { #[pymethod] fn flush(&self, vm: &VirtualMachine) -> PyResult { let mut textio = self.lock(vm)?; - flush_inner(&mut textio, vm) + let buffer = text_buffer(&self.buffer, vm)?; + flush_inner(&mut textio, &buffer, vm) } #[pymethod] fn truncate(zelf: PyRef, pos: KeepNonePos, vm: &VirtualMachine) -> PyResult { // Implementation follows _pyio.py TextIOWrapper.truncate let mut textio = zelf.lock(vm)?; - flush_inner(&mut textio, vm)?; - let buffer = textio.buffer.clone(); + let buffer = text_buffer(&zelf.buffer, vm)?; + flush_inner(&mut textio, &buffer, vm)?; drop(textio); let pos = match pos.pos.into_option() { @@ -3875,8 +4002,9 @@ mod _io { #[pymethod] fn isatty(&self, vm: &VirtualMachine) -> PyResult { let textio = self.lock(vm)?; - textio.check_closed(vm)?; - vm.call_method(&textio.buffer, "isatty", ()) + let buffer = text_buffer(&self.buffer, vm)?; + textio.check_closed(&buffer, vm)?; + vm.call_method(&buffer, "isatty", ()) } #[pymethod] @@ -3884,9 +4012,10 @@ mod _io { let limit = size.to_usize(); let mut textio = self.lock(vm)?; - check_closed(&textio.buffer, vm)?; + let buffer = text_buffer(&self.buffer, vm)?; + check_closed(&buffer, vm)?; - textio.write_pending(vm)?; + textio.write_pending(&buffer, vm)?; #[derive(Clone)] struct SlicedStr(PyStrRef, Range); @@ -3947,7 +4076,7 @@ mod _io { Some(s) if !s.is_empty() => break s, _ => {} } - let eof = textio.read_chunk(0, vm)?; + let eof = textio.read_chunk(&buffer, 0, vm)?; if eof { textio.set_decoded_chars(None); textio.snapshot = None; @@ -4050,7 +4179,11 @@ mod _io { #[pymethod] fn close(zelf: PyRef, vm: &VirtualMachine) -> PyResult<()> { - let buffer = zelf.lock(vm)?.buffer.clone(); + let _textio = zelf.lock(vm)?; + let Some(buffer) = zelf.buffer.load_owned() else { + return Err(vm.new_value_error("underlying buffer has been detached")); + }; + drop(_textio); if file_closed(&buffer, vm)? { return Ok(()); } @@ -4058,7 +4191,7 @@ mod _io { // The file_closed() check above may have triggered a reentrant // call to detach() via a custom `closed` property. // If so, the buffer is now detached and we should return early. - if vm.is_none(&zelf.lock(vm)?.buffer) { + if zelf.buffer.deref().is_none() { return Ok(()); } if zelf.finalizing.load(Ordering::Relaxed) { @@ -4072,13 +4205,8 @@ mod _io { #[pygetset] fn closed(&self, vm: &VirtualMachine) -> PyResult { - let buffer = self.lock(vm)?.buffer.clone(); - buffer.get_attr("closed", vm) - } - - #[pygetset] - fn buffer(&self, vm: &VirtualMachine) -> PyResult { - Ok(self.lock(vm)?.buffer.clone()) + let _textio = self.lock(vm)?; + text_buffer(&self.buffer, vm)?.get_attr("closed", vm) } #[pymethod] @@ -4119,17 +4247,22 @@ mod _io { } impl TextIOData { - fn write_pending(&mut self, vm: &VirtualMachine) -> PyResult<()> { + fn write_pending(&mut self, buffer: &PyObject, vm: &VirtualMachine) -> PyResult<()> { if self.pending.num_bytes == 0 { return Ok(()); } let data = self.pending.take(vm); - vm.call_method(&self.buffer, "write", (data,))?; + vm.call_method(buffer, "write", (data,))?; Ok(()) } /// returns true on EOF - fn read_chunk(&mut self, size_hint: usize, vm: &VirtualMachine) -> PyResult { + fn read_chunk( + &mut self, + buffer: &PyObject, + size_hint: usize, + vm: &VirtualMachine, + ) -> PyResult { let decoder = self .decoder .as_ref() @@ -4149,7 +4282,7 @@ mod _io { size_hint }; let chunk_size = core::cmp::max(self.chunk_size, size_hint); - let input_chunk = vm.call_method(&self.buffer, method, (chunk_size,))?; + let input_chunk = vm.call_method(buffer, method, (chunk_size,))?; let buf = ArgBytesLike::try_from_borrowed_object(vm, &input_chunk).map_err(|_| { vm.new_type_error(format!( @@ -4182,8 +4315,8 @@ mod _io { Ok(eof) } - fn check_closed(&self, vm: &VirtualMachine) -> PyResult<()> { - check_closed(&self.buffer, vm) + fn check_closed(&self, buffer: &PyObject, vm: &VirtualMachine) -> PyResult<()> { + check_closed(buffer, vm) } /// returns str, str.char_len() (it might not be cached in the str yet but we calculate it @@ -4282,14 +4415,17 @@ mod _io { // Reentrant call return Ok(vm.ctx.new_str(Wtf8Buf::from(format!("<{type_name}>")))); }; - let Some(data) = data.as_ref() else { + if data.is_none() { return Err(vm.new_value_error("I/O operation on uninitialized object")); - }; + } let mut result = Wtf8Buf::from(format!("<{type_name}")); + let buffer = zelf.buffer.load_owned(); // Add name if present - if let Ok(Some(name)) = vm.get_attribute_opt(&data.buffer, "name") { + if let Some(buffer) = &buffer + && let Ok(Some(name)) = vm.get_attribute_opt(buffer, "name") + { let name_repr = name.repr(vm)?; result.push_wtf8(" name=".as_ref()); result.push_wtf8(name_repr.as_wtf8()); @@ -4298,9 +4434,12 @@ mod _io { // Add mode if present (prefer the wrapper's attribute) let mode_obj = match vm.get_attribute_opt(zelf.as_object(), "mode") { Ok(Some(mode)) => Some(mode), - Ok(None) | Err(_) => match vm.get_attribute_opt(&data.buffer, "mode") { - Ok(Some(mode)) => Some(mode), - _ => None, + Ok(None) | Err(_) => match buffer.as_ref() { + Some(buffer) => match vm.get_attribute_opt(buffer, "mode") { + Ok(Some(mode)) => Some(mode), + _ => None, + }, + None => None, }, }; if let Some(mode) = mode_obj { @@ -4310,8 +4449,9 @@ mod _io { } // Add encoding (always valid UTF-8) + let encoding = textio_encoding(&zelf.encoding, vm)?; result.push_wtf8(" encoding='".as_ref()); - result.push_wtf8(data.encoding.as_str().as_ref()); + result.push_wtf8(encoding.as_str().as_ref()); result.push_wtf8("'>".as_ref()); Ok(vm.ctx.new_str(result)) @@ -5438,7 +5578,9 @@ mod _io { if let Some(ref decoder) = data.decoder { reinit_io_locks(decoder); } - reinit_io_locks(&data.buffer); + if let Some(buffer) = tio.buffer.load_owned() { + reinit_io_locks(&buffer); + } } return; } diff --git a/crates/vm/src/stdlib/_sre.rs b/crates/vm/src/stdlib/_sre.rs index 88e380f40..a223c839c 100644 --- a/crates/vm/src/stdlib/_sre.rs +++ b/crates/vm/src/stdlib/_sre.rs @@ -263,7 +263,15 @@ mod _sre { items: Vec<(usize, PyObjectRef)>, } - #[pyclass] + /// One `TemplateObject.items` element: `Py_ssize_t` index, object pointer. + #[repr(C)] + #[allow(dead_code)] + struct SreTemplateItem { + _index: isize, + _literal: *mut PyObject, + } + + #[pyclass(itemsize = core::mem::size_of::())] impl Template { fn compile( pattern: PyRef, @@ -378,7 +386,11 @@ mod _sre { }}; } - #[pyclass(with(Hashable, Comparable, Representable), flags(HAS_WEAKREF))] + #[pyclass( + itemsize = core::mem::size_of::(), + with(Hashable, Comparable, Representable), + flags(HAS_WEAKREF) + )] impl Pattern { fn downcast_str<'a>(string: &'a PyObject, vm: &VirtualMachine) -> PyResult<&'a Py> { string.downcast_ref::().ok_or_else(|| { @@ -817,7 +829,11 @@ mod _sre { regs: Vec<(isize, isize)>, } - #[pyclass(with(AsMapping, Representable), flags(DISALLOW_INSTANTIATION))] + #[pyclass( + itemsize = core::mem::size_of::(), + with(AsMapping, Representable), + flags(DISALLOW_INSTANTIATION) + )] impl Match { pub(crate) fn new(state: &mut State, pattern: PyRef, string: PyObjectRef) -> Self { let string_position = state.cursor.position; diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index 5721fab56..6896f6e3b 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -31,7 +31,7 @@ pub(crate) mod _thread { OptionalArg, Param, ParamKind, PosArgs, PySetterValue, TimeoutSeconds, }, object::{Traverse, TraverseFn}, - types::{Constructor, GetAttr, Representable, SetAttr}, + types::{Constructor, GetAttr, PyStructSequence, Representable, SetAttr}, }; use alloc::sync::{Arc, Weak}; @@ -1000,81 +1000,69 @@ pub(crate) mod _thread { } // This allows threading.py to import _excepthook and _ExceptHookArgs from _thread - #[pyattr] - #[pyclass(module = "_thread", name = "_ExceptHookArgs")] - #[derive(Debug, PyPayload)] - struct ExceptHookArgs { + #[pystruct_sequence_data] + struct ExceptHookArgsData { exc_type: crate::PyObjectRef, exc_value: crate::PyObjectRef, exc_traceback: crate::PyObjectRef, thread: crate::PyObjectRef, } - #[pyclass(with(Constructor))] - impl ExceptHookArgs { - #[pygetset] - fn exc_type(&self) -> crate::PyObjectRef { - self.exc_type.clone() - } + #[pyattr] + #[pystruct_sequence( + name = "_ExceptHookArgs", + module = "_thread", + data = "ExceptHookArgsData" + )] + struct PyExceptHookArgs; - #[pygetset] - fn exc_value(&self) -> crate::PyObjectRef { - self.exc_value.clone() - } - - #[pygetset] - fn exc_traceback(&self) -> crate::PyObjectRef { - self.exc_traceback.clone() - } - - #[pygetset] - fn thread(&self) -> crate::PyObjectRef { - self.thread.clone() - } - } - - #[derive(FromArgs)] - struct ExceptHookNewArgs { - #[pyarg(positional, default, py_default = "()")] - iterable: crate::function::OptionalArg, - } - - impl Constructor for ExceptHookArgs { - // Takes a single iterable argument like namedtuple - type Args = ExceptHookNewArgs; - - fn py_new(_cls: &Py, args: Self::Args, vm: &VirtualMachine) -> PyResult { - // Convert the argument to a list/tuple and extract elements - let seq: Vec = match args.iterable { - crate::function::OptionalArg::Present(iterable) => iterable.try_to_value(vm)?, - crate::function::OptionalArg::Missing => Vec::new(), - }; - if seq.len() != 4 { - return Err(vm.new_type_error(format!( - "_ExceptHookArgs expected 4 arguments, got {}", - seq.len() - ))); - } - Ok(Self { - exc_type: seq[0].clone(), - exc_value: seq[1].clone(), - exc_traceback: seq[2].clone(), - thread: seq[3].clone(), - }) + #[pyclass(with(PyStructSequence))] + impl PyExceptHookArgs { + #[extend_class] + fn extend_pyclass(ctx: &crate::vm::Context, class: &'static Py) { + // (type, (sequence, dict)). Set before the trait installs its default. + const EXCEPT_HOOK_ARGS_REDUCE: crate::function::PyMethodDef = + crate::function::PyMethodDef::new_const( + "__reduce__", + |zelf: crate::PyRef, + vm: &VirtualMachine| + -> PyTupleRef { + vm.new_tuple(( + zelf.class().to_owned(), + ( + vm.ctx.new_tuple(zelf.as_slice().to_vec()), + vm.ctx.new_dict(), + ), + )) + }, + crate::function::PyMethodFlags::METHOD, + crate::function::ItemDoc::NONE, + ); + class.set_attr( + ctx.intern_str("__reduce__"), + EXCEPT_HOOK_ARGS_REDUCE.to_proper_method(class, ctx), + ); } } #[pyfunction] fn _excepthook(args: crate::PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { // Type check: args must be _ExceptHookArgs - let args = args.downcast::().map_err(|_| { - vm.new_type_error("_thread._excepthook argument type must be _ExceptHookArgs") + let args = args.downcast::().map_err(|_| { + vm.new_type_error("_thread.excepthook argument type must be ExceptHookArgs") })?; - - let exc_type = args.exc_type.clone(); - let exc_value = args.exc_value.clone(); - let exc_traceback = args.exc_traceback.clone(); - let thread = args.thread.clone(); + let fields = args.0.as_slice(); + let (Some(exc_type), Some(exc_value), Some(exc_traceback), Some(thread)) = + (fields.first(), fields.get(1), fields.get(2), fields.get(3)) + else { + return Err( + vm.new_type_error("_thread.excepthook argument type must be ExceptHookArgs") + ); + }; + let exc_type = exc_type.clone(); + let exc_value = exc_value.clone(); + let exc_traceback = exc_traceback.clone(); + let thread = thread.clone(); // Silently ignore SystemExit (identity check) if exc_type.is(vm.ctx.exceptions.system_exit.as_ref()) { diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 116b2e91a..6914d6243 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -1187,7 +1187,7 @@ pub mod sys { ); // Print module name (if not builtins or __main__) - let module_name = unraisable.exc_type.__module__(vm); + let module_name = unraisable.exc_type.__module__(vm)?; if let Ok(module_str) = module_name.downcast::() { let module = module_str.as_wtf8(); if module != "builtins" && module != "__main__" { diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index 4deea4b61..2c2ad8efc 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -134,6 +134,8 @@ pub struct PyTypeSlots { /// For heap types, `__name__` must alive pub(crate) name: &'static str, // tp_name with . for print, not class name + /// Full `tp_basicsize`: object header plus payload. `0` before type + /// creation means "inherit the base size". pub basicsize: usize, pub itemsize: usize, // tp_itemsize @@ -171,8 +173,9 @@ pub struct PyTypeSlots { pub methods: &'static [PyMethodDef], - // Flags to define presence of optional/expanded features - pub flags: PyTypeFlags, + // Flags to define presence of optional/expanded features. + // One atomic word: runtime code sets and clears bits in place. + pub flags: PyAtomicTypeFlags, // tp_doc pub doc: ItemDoc, @@ -215,7 +218,7 @@ impl PyTypeSlots { pub fn new(name: &'static str, flags: PyTypeFlags) -> Self { Self { name, - flags, + flags: PyAtomicTypeFlags::new(flags), ..Default::default() } } @@ -300,6 +303,107 @@ impl Default for PyTypeFlags { } } +/// `tp_flags` as one atomic word. Reads match [`PyTypeFlags`]; bits that change +/// after the type is published are set and cleared in place. +#[repr(transparent)] +pub struct PyAtomicTypeFlags(core::sync::atomic::AtomicU64); + +impl core::fmt::Debug for PyAtomicTypeFlags { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.load().fmt(f) + } +} + +impl Default for PyAtomicTypeFlags { + fn default() -> Self { + Self::new(PyTypeFlags::DEFAULT) + } +} + +impl PyAtomicTypeFlags { + #[must_use] + pub const fn new(flags: PyTypeFlags) -> Self { + Self(core::sync::atomic::AtomicU64::new(flags.bits())) + } + + #[must_use] + pub fn load(&self) -> PyTypeFlags { + PyTypeFlags::from_bits_truncate(self.bits()) + } + + #[must_use] + pub fn bits(&self) -> u64 { + self.0.load(core::sync::atomic::Ordering::Acquire) + } + + #[must_use] + pub fn has_feature(&self, flag: PyTypeFlags) -> bool { + self.load().has_feature(flag) + } + + #[must_use] + pub fn contains(&self, flag: PyTypeFlags) -> bool { + self.load().contains(flag) + } + + #[must_use] + pub fn intersects(&self, flag: PyTypeFlags) -> bool { + self.load().intersects(flag) + } + + pub fn remove(&self, flag: PyTypeFlags) { + self.0 + .fetch_and(!flag.bits(), core::sync::atomic::Ordering::AcqRel); + } + + pub fn set(&self, flag: PyTypeFlags) { + self.0 + .fetch_or(flag.bits(), core::sync::atomic::Ordering::AcqRel); + } + + /// Replace `mask` bits with `value & mask`. + pub fn replace_masked(&self, mask: PyTypeFlags, value: PyTypeFlags) { + let mask_bits = mask.bits(); + let value_bits = (value & mask).bits(); + let _ = self.0.fetch_update( + core::sync::atomic::Ordering::AcqRel, + core::sync::atomic::Ordering::Acquire, + |old| Some((old & !mask_bits) | value_bits), + ); + } + + #[cfg(debug_assertions)] + #[must_use] + pub fn is_created_with_flags(&self) -> bool { + self.load().is_created_with_flags() + } +} + +impl core::ops::BitOrAssign for PyAtomicTypeFlags { + fn bitor_assign(&mut self, rhs: PyTypeFlags) { + self.set(rhs); + } +} + +const _: () = assert!( + core::mem::size_of::() + == core::mem::size_of::() + && core::mem::align_of::() + == core::mem::align_of::() + && core::mem::size_of::() == core::mem::size_of::() + && core::mem::align_of::() == core::mem::align_of::() +); + +impl crate::builtins::descriptor::MemberLayout for PyAtomicTypeFlags { + const KIND: crate::builtins::descriptor::MemberKind = { + if core::mem::size_of::() == 8 { + crate::builtins::descriptor::MemberKind::ULong + } else { + crate::builtins::descriptor::MemberKind::ULongLong + } + }; +} + pub(crate) type GenericMethod = fn(&PyObject, FuncArgs, &VirtualMachine) -> PyResult; /// Vectorcall function pointer (PEP 590). /// args: owned positional args followed by kwarg values. diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 508813183..781525ad7 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -622,7 +622,9 @@ impl Context { let slots = PyTypeSlots { name: interned_name.as_str(), basicsize: 0, - flags: PyTypeFlags::heap_type_flags() | PyTypeFlags::HAS_DICT, + flags: crate::types::PyAtomicTypeFlags::new( + PyTypeFlags::heap_type_flags() | PyTypeFlags::HAS_DICT, + ), ..PyTypeSlots::default() }; PyType::new_heap( diff --git a/crates/vm/src/vm/vm_new.rs b/crates/vm/src/vm/vm_new.rs index f8efe16a9..e814474bf 100644 --- a/crates/vm/src/vm/vm_new.rs +++ b/crates/vm/src/vm/vm_new.rs @@ -434,7 +434,7 @@ impl VirtualMachine { ) -> PyBaseExceptionRef { debug_assert_eq!( exc_type.slots.basicsize, - core::mem::size_of::(), + crate::object::SIZEOF_PYOBJECT_HEAD + core::mem::size_of::(), "vm.new_simple_exception() requires a BaseException-sized type, got {}", exc_type.name() ); @@ -452,7 +452,9 @@ impl VirtualMachine { /// [`invoke_exception`][Self::invoke_exception] or /// [`exceptions::ExceptionCtor`][crate::exceptions::ExceptionCtor]. pub fn new_exception(&self, exc_type: PyTypeRef, args: Vec) -> PyBaseExceptionRef { - if exc_type.slots.basicsize == core::mem::size_of::() { + if exc_type.slots.basicsize + == crate::object::SIZEOF_PYOBJECT_HEAD + core::mem::size_of::() + { self.new_simple_exception(exc_type, args) } else { self.invoke_exception(&exc_type, args).unwrap_or_else(|e| e) @@ -468,7 +470,7 @@ impl VirtualMachine { { debug_assert_eq!( cls.slots.basicsize, - size_of::(), + crate::object::SIZEOF_PYOBJECT_HEAD + size_of::(), "vm.new_payload_exception::<{}>() called with mismatched type '{}'", core::any::type_name::(), cls.name() @@ -494,7 +496,10 @@ impl VirtualMachine { errno: Option, msg: impl ToPyObject, ) -> PyRef { - debug_assert_eq!(exc_type.slots.basicsize, core::mem::size_of::()); + debug_assert_eq!( + exc_type.slots.basicsize, + crate::object::SIZEOF_PYOBJECT_HEAD + core::mem::size_of::() + ); OSErrorBuilder::with_subtype(exc_type, errno, msg, self).build(self) }