diff --git a/newsfragments/6016.added.md b/newsfragments/6016.added.md new file mode 100644 index 00000000000..6f63b236e47 --- /dev/null +++ b/newsfragments/6016.added.md @@ -0,0 +1 @@ +Added FFI wrappers for [PEP 757](https://peps.python.org/pep-0757/) `PyLong` import / export API on Python 3.14. \ No newline at end of file diff --git a/pyo3-ffi/src/cpython/longintrepr.rs b/pyo3-ffi/src/cpython/longintrepr.rs new file mode 100644 index 00000000000..427f067178c --- /dev/null +++ b/pyo3-ffi/src/cpython/longintrepr.rs @@ -0,0 +1,55 @@ +use crate::{PyObject, Py_ssize_t}; +use core::ffi::{c_int, c_void}; + +use crate::Py_uintptr_t; + +// skipped PyLong_BASE +// skipped PyLong_MASK +// skipped _PyLong_New +// skipped _PyLong_Copy +// skipped _PyLong_FromDigits +// skipped _PyLong_SIGN_MASK +// skipped _PyLong_NON_SIZE_BITS +// skipped PyUnstable_Long_IsCompact +// skipped PyUnstable_Long_CompactValue + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct PyLongLayout { + pub bits_per_digit: u8, + pub digit_size: u8, + pub digits_order: i8, + pub digit_endianness: i8, +} + +extern_libpython! { + pub fn PyLong_GetNativeLayout() -> *const PyLongLayout; +} + +#[repr(C)] +pub struct PyLongExport { + pub value: i64, + pub negative: u8, + pub ndigits: Py_ssize_t, + pub digits: *const c_void, + _reserved: Py_uintptr_t, +} + +extern_libpython! { + pub fn PyLong_Export(obj: *mut PyObject, export_long: *mut PyLongExport) -> c_int; + pub fn PyLong_FreeExport(export_long: *mut PyLongExport); +} + +opaque_struct!(pub PyLongWriter); + +extern_libpython! { + pub fn PyLongWriter_Create( + negative: c_int, + ndigits: Py_ssize_t, + digits: *mut *mut c_void, + ) -> *mut PyLongWriter; + + pub fn PyLongWriter_Finish(writer: *mut PyLongWriter) -> *mut PyObject; + + pub fn PyLongWriter_Discard(writer: *mut PyLongWriter); +} diff --git a/pyo3-ffi/src/cpython/longobject.rs b/pyo3-ffi/src/cpython/longobject.rs index ef0e2c465b1..88aea6d0d27 100644 --- a/pyo3-ffi/src/cpython/longobject.rs +++ b/pyo3-ffi/src/cpython/longobject.rs @@ -1,57 +1,31 @@ -use crate::longobject::*; -use crate::object::*; -#[cfg(Py_3_13)] -use crate::pyport::Py_ssize_t; -#[cfg(Py_3_13)] -use core::ffi::c_void; +use crate::{longobject::*, object::*}; use core::ffi::{c_int, c_uchar}; use libc::size_t; -#[cfg(Py_3_13)] +// skipped _PyLong_CAST + extern_libpython! { + #[cfg(Py_3_13)] pub fn PyLong_FromUnicodeObject(u: *mut PyObject, base: c_int) -> *mut PyObject; -} - -#[cfg(Py_3_13)] -pub const Py_ASNATIVEBYTES_DEFAULTS: c_int = -1; -#[cfg(Py_3_13)] -pub const Py_ASNATIVEBYTES_BIG_ENDIAN: c_int = 0; -#[cfg(Py_3_13)] -pub const Py_ASNATIVEBYTES_LITTLE_ENDIAN: c_int = 1; -#[cfg(Py_3_13)] -pub const Py_ASNATIVEBYTES_NATIVE_ENDIAN: c_int = 3; -#[cfg(Py_3_13)] -pub const Py_ASNATIVEBYTES_UNSIGNED_BUFFER: c_int = 4; -#[cfg(Py_3_13)] -pub const Py_ASNATIVEBYTES_REJECT_NEGATIVE: c_int = 8; -extern_libpython! { - // skipped _PyLong_Sign + // skipped PyUnstable_Long_IsCompact + // skipped PyUnstable_Long_CompactValue - #[cfg(Py_3_13)] - pub fn PyLong_AsNativeBytes( - v: *mut PyObject, - buffer: *mut c_void, - n_bytes: Py_ssize_t, - flags: c_int, - ) -> Py_ssize_t; + #[cfg(Py_3_14)] + pub fn PyLong_IsPositive(obj: *mut PyObject) -> c_int; + #[cfg(Py_3_14)] + pub fn PyLong_IsNegative(obj: *mut PyObject) -> c_int; + #[cfg(Py_3_14)] + pub fn PyLong_IsZero(obj: *mut PyObject) -> c_int; - #[cfg(Py_3_13)] - pub fn PyLong_FromNativeBytes( - buffer: *const c_void, - n_bytes: size_t, - flags: c_int, - ) -> *mut PyObject; + // skipped PyLong_GetSign - #[cfg(Py_3_13)] - pub fn PyLong_FromUnsignedNativeBytes( - buffer: *const c_void, - n_bytes: size_t, - flags: c_int, - ) -> *mut PyObject; + // skipped _PyLong_Sign - // skipped PyUnstable_Long_IsCompact - // skipped PyUnstable_Long_CompactValue + #[cfg_attr(PyPy, link_name = "_PyPyLong_NumBits")] + #[cfg(not(Py_3_13))] + #[doc(hidden)] + pub fn _PyLong_NumBits(obj: *mut PyObject) -> size_t; #[cfg_attr(PyPy, link_name = "_PyPyLong_FromByteArray")] pub fn _PyLong_FromByteArray( diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 3590c9f4d38..2705740d832 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -24,6 +24,8 @@ pub(crate) mod initconfig; pub(crate) mod listobject; #[cfg(Py_3_13)] pub(crate) mod lock; +#[cfg(Py_3_14)] +pub(crate) mod longintrepr; pub(crate) mod longobject; pub(crate) mod marshal; #[cfg(all(Py_3_9, not(PyPy)))] @@ -71,6 +73,8 @@ pub use self::initconfig::*; pub use self::listobject::*; #[cfg(Py_3_13)] pub use self::lock::*; +#[cfg(Py_3_14)] +pub use self::longintrepr::*; pub use self::longobject::*; pub use self::marshal::*; #[cfg(all(Py_3_9, not(PyPy)))] diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 8f63e7f26ed..f77cc003d6d 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -536,7 +536,6 @@ mod import; mod intrcheck; mod iterobject; mod listobject; -// skipped longintrepr.h mod longobject; mod memoryobject; mod methodobject; diff --git a/pyo3-ffi/src/longobject.rs b/pyo3-ffi/src/longobject.rs index fda262ba0af..7c777fecc14 100644 --- a/pyo3-ffi/src/longobject.rs +++ b/pyo3-ffi/src/longobject.rs @@ -45,7 +45,66 @@ extern_libpython! { pub fn PyLong_AsUnsignedLong(arg1: *mut PyObject) -> c_ulong; #[cfg_attr(PyPy, link_name = "PyPyLong_AsUnsignedLongMask")] pub fn PyLong_AsUnsignedLongMask(arg1: *mut PyObject) -> c_ulong; - // skipped non-limited _PyLong_AsInt + + // skipped non-limited PyLong_AsInt + + #[cfg(Py_3_14)] + pub fn PyLong_FromInt32(arg1: i32) -> *mut PyObject; + #[cfg(Py_3_14)] + pub fn PyLong_FromUInt32(arg1: u32) -> *mut PyObject; + #[cfg(Py_3_14)] + pub fn PyLong_FromInt64(arg1: i64) -> *mut PyObject; + #[cfg(Py_3_14)] + pub fn PyLong_FromUInt64(arg1: u64) -> *mut PyObject; + + #[cfg(Py_3_14)] + pub fn PyLong_AsInt32(arg1: *mut PyObject, arg2: *mut i32) -> c_int; + #[cfg(Py_3_14)] + pub fn PyLong_AsUInt32(arg1: *mut PyObject, arg2: *mut u32) -> c_int; + #[cfg(Py_3_14)] + pub fn PyLong_AsInt64(arg1: *mut PyObject, arg2: *mut i64) -> c_int; + #[cfg(Py_3_14)] + pub fn PyLong_AsUInt64(arg1: *mut PyObject, arg2: *mut u64) -> c_int; +} + +#[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] +pub const Py_ASNATIVEBYTES_DEFAULTS: c_int = -1; +#[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] +pub const Py_ASNATIVEBYTES_BIG_ENDIAN: c_int = 0; +#[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] +pub const Py_ASNATIVEBYTES_LITTLE_ENDIAN: c_int = 1; +#[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] +pub const Py_ASNATIVEBYTES_NATIVE_ENDIAN: c_int = 3; +#[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] +pub const Py_ASNATIVEBYTES_UNSIGNED_BUFFER: c_int = 4; +#[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] +pub const Py_ASNATIVEBYTES_REJECT_NEGATIVE: c_int = 8; +#[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] +pub const Py_ASNATIVEBYTES_ALLOW_INDEX: c_int = 16; + +extern_libpython! { + #[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] + pub fn PyLong_AsNativeBytes( + v: *mut PyObject, + buffer: *mut c_void, + n_bytes: Py_ssize_t, + flags: c_int, + ) -> Py_ssize_t; + + #[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] + pub fn PyLong_FromNativeBytes( + buffer: *const c_void, + n_bytes: size_t, + flags: c_int, + ) -> *mut PyObject; + + #[cfg(any(Py_3_14, all(Py_3_13, not(Py_LIMITED_API))))] + pub fn PyLong_FromUnsignedNativeBytes( + buffer: *const c_void, + n_bytes: size_t, + flags: c_int, + ) -> *mut PyObject; + pub fn PyLong_GetInfo() -> *mut PyObject; // skipped PyLong_AS_LONG @@ -54,15 +113,6 @@ extern_libpython! { // skipped _Py_PARSE_INTPTR // skipped _Py_PARSE_UINTPTR - // skipped non-limited _PyLong_UnsignedShort_Converter - // skipped non-limited _PyLong_UnsignedInt_Converter - // skipped non-limited _PyLong_UnsignedLong_Converter - // skipped non-limited _PyLong_UnsignedLongLong_Converter - // skipped non-limited _PyLong_Size_t_Converter - - // skipped non-limited _PyLong_DigitValue - // skipped non-limited _PyLong_Frexp - #[cfg_attr(PyPy, link_name = "PyPyLong_AsDouble")] pub fn PyLong_AsDouble(arg1: *mut PyObject) -> c_double; #[cfg_attr(PyPy, link_name = "PyPyLong_FromVoidPtr")] @@ -89,23 +139,7 @@ extern_libpython! { ) -> *mut PyObject; } -#[cfg(not(Py_LIMITED_API))] -extern_libpython! { - #[cfg_attr(PyPy, link_name = "_PyPyLong_NumBits")] - #[cfg(not(Py_3_13))] - #[doc(hidden)] - pub fn _PyLong_NumBits(obj: *mut PyObject) -> size_t; -} - -// skipped non-limited _PyLong_Format -// skipped non-limited _PyLong_FormatWriter -// skipped non-limited _PyLong_FormatBytesWriter -// skipped non-limited _PyLong_FormatAdvancedWriter - extern_libpython! { pub fn PyOS_strtoul(arg1: *const c_char, arg2: *mut *mut c_char, arg3: c_int) -> c_ulong; pub fn PyOS_strtol(arg1: *const c_char, arg2: *mut *mut c_char, arg3: c_int) -> c_long; } - -// skipped non-limited _PyLong_Rshift -// skipped non-limited _PyLong_Lshift diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index fb122c279ef..e6968661a74 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -353,6 +353,93 @@ int_convert_u64_or_i64!( true ); +#[cfg(all(Py_3_14, not(Py_LIMITED_API)))] +pub(crate) const PYLONG_BITS_IN_DIGIT: usize = 30; + +#[cfg(all(Py_3_14, not(Py_LIMITED_API)))] +pub(crate) fn is_30bit_layout() -> bool { + static DIGITS: std::sync::OnceLock = std::sync::OnceLock::new(); + + const PYLONG_DIGIT_SIZE: u8 = 4; + const PYLONG_DIGITS_ORDER: i8 = -1; + + #[cfg(target_endian = "little")] + const NATIVE_DIGIT_ENDIANNESS: i8 = -1; + #[cfg(target_endian = "big")] + const NATIVE_DIGIT_ENDIANNESS: i8 = 1; + + *DIGITS.get_or_init(|| { + let layout = unsafe { &*ffi::PyLong_GetNativeLayout() }; + layout.bits_per_digit == PYLONG_BITS_IN_DIGIT as u8 + && layout.digit_size == PYLONG_DIGIT_SIZE + && layout.digits_order == PYLONG_DIGITS_ORDER + && layout.digit_endianness == NATIVE_DIGIT_ENDIANNESS + }) +} + +#[cfg(all(Py_3_14, not(Py_LIMITED_API)))] +struct ExportGuard(ffi::PyLongExport); + +#[cfg(all(Py_3_14, not(Py_LIMITED_API)))] +impl Drop for ExportGuard { + fn drop(&mut self) { + unsafe { ffi::PyLong_FreeExport(&mut self.0) }; + } +} + +// Builds an int from an iterator of 30-bit digits +#[cfg(all(Py_3_14, not(Py_LIMITED_API)))] +#[inline] +pub(crate) fn pylong_from_digits<'py, I: ExactSizeIterator>( + py: Python<'py>, + negative: bool, + digits: I, +) -> Bound<'py, PyInt> { + let digits_len = digits.len(); + let mut ptr = std::ptr::null_mut(); + let writer = unsafe { + ffi::PyLongWriter_Create(negative.into(), digits_len as ffi::Py_ssize_t, &mut ptr) + }; + assert!(!writer.is_null(), "PyLongWriter_Create returned NULL"); + let digit_ptr = ptr.cast::(); + for (i, d) in digits.enumerate() { + unsafe { digit_ptr.add(i).write(d) }; + } + unsafe { + ffi::PyLongWriter_Finish(writer) + .assume_owned(py) + .cast_into_unchecked() + } +} + +// Visits 30-bit digits LSB-first and deals with freeing the export +#[cfg(all(Py_3_14, not(Py_LIMITED_API)))] +#[inline] +pub(crate) fn pylong_visit_digits( + obj: Borrowed<'_, '_, PyAny>, + f: impl FnOnce(bool, i64, Option<&[u32]>) -> PyResult, +) -> PyResult { + let mut long_export = MaybeUninit::::uninit(); + unsafe { + crate::err::error_on_minusone( + obj.py(), + ffi::PyLong_Export(obj.as_ptr(), long_export.as_mut_ptr()), + )?; + } + let export_guard = ExportGuard(unsafe { long_export.assume_init() }); + let long_export_ref = &export_guard.0; + let negative = long_export_ref.negative != 0; + let value = long_export_ref.value; + if long_export_ref.digits.is_null() { + f(negative, value, None) + } else { + let n_digits = long_export_ref.ndigits as usize; + let ptr = long_export_ref.digits.cast::(); + let digits = unsafe { std::slice::from_raw_parts(ptr, n_digits) }; + f(negative, value, Some(digits)) + } +} + #[cfg(not(Py_LIMITED_API))] mod fast_128bit_int_conversion { use super::*; @@ -369,6 +456,24 @@ mod fast_128bit_int_conversion { const OUTPUT_TYPE: PyStaticExpr = PyInt::TYPE_HINT; fn into_pyobject(self, py: Python<'py>) -> Result { + #[cfg(Py_3_14)] + { + if is_30bit_layout() { + const DIGIT_MASK: u32 = (1 << PYLONG_BITS_IN_DIGIT) - 1; + let signed = self as i128; + let negative = $is_signed && signed < 0; + let abs = if negative { + signed.unsigned_abs() + } else { + self as u128 + }; + let bits = 128 - abs.leading_zeros() as usize; + let n_digits = bits.div_ceil(PYLONG_BITS_IN_DIGIT).max(1); + let digits = (0..n_digits) + .map(|i| (abs >> (i * PYLONG_BITS_IN_DIGIT)) as u32 & DIGIT_MASK); + return Ok(pylong_from_digits(py, negative, digits)); + } + } #[cfg(Py_3_13)] { let bytes = self.to_ne_bytes(); @@ -404,6 +509,58 @@ mod fast_128bit_int_conversion { fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let num = nb_index(&ob)?; + #[cfg(Py_3_14)] + { + if is_30bit_layout() { + let overflow = || { + exceptions::PyOverflowError::new_err( + "Python int larger than 128 bits", + ) + }; + return pylong_visit_digits( + num.as_any().as_borrowed(), + |negative, compact, digits| { + if !$is_signed && negative { + return Err(exceptions::PyOverflowError::new_err( + "can't convert negative int to unsigned", + )); + } + let Some(digits) = digits else { + return <$rust_type>::try_from(compact) + .map_err(|_| overflow()); + }; + let n_digits = digits.len(); + let mut abs = 0_u128; + let mut overflowed = n_digits > 5; + for (i, &digit) in digits.iter().take(5).enumerate() { + let d = u128::from(digit); + if i == 4 && d >> 8 != 0 { + overflowed = true; + } + abs |= d << (i * PYLONG_BITS_IN_DIGIT); + } + if overflowed { + return Err(overflow()); + } + if !$is_signed { + return <$rust_type>::try_from(abs).map_err(|_| overflow()); + } + let signed = if negative { + if abs > 1_u128 << 127 { + return Err(overflow()); + } + (abs.wrapping_neg()) as i128 + } else { + if abs > i128::MAX as u128 { + return Err(overflow()); + } + abs as i128 + }; + <$rust_type>::try_from(signed).map_err(|_| overflow()) + }, + ); + } + } let mut buffer = [0u8; std::mem::size_of::<$rust_type>()]; #[cfg(not(Py_3_13))] { @@ -754,7 +911,7 @@ mod test_128bit_integers { Python::attach(|py| { let obj = py.eval(c"(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance_of::(py)); + assert!(err.is_instance_of::(py)); }) } @@ -763,7 +920,29 @@ mod test_128bit_integers { Python::attach(|py| { let obj = py.eval(c"1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance_of::(py)); + assert!(err.is_instance_of::(py)); + }) + } + + #[test] + fn test_u128_negative() { + Python::attach(|py| { + let obj = py.eval(c"-1", None, None).unwrap(); + let err = obj.extract::().unwrap_err(); + assert!(err.is_instance_of::(py)); + }) + } + + #[test] + fn test_i128_boundary_overflow() { + Python::attach(|py| { + let obj = py.eval(c"-(2**127) - 1", None, None).unwrap(); + let err = obj.extract::().unwrap_err(); + assert!(err.is_instance_of::(py)); + + let obj = py.eval(c"2**127", None, None).unwrap(); + let err = obj.extract::().unwrap_err(); + assert!(err.is_instance_of::(py)); }) } @@ -807,7 +986,7 @@ mod test_128bit_integers { Python::attach(|py| { let obj = py.eval(c"(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance_of::(py)); + assert!(err.is_instance_of::(py)); }) } @@ -816,7 +995,7 @@ mod test_128bit_integers { Python::attach(|py| { let obj = py.eval(c"1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance_of::(py)); + assert!(err.is_instance_of::(py)); }) } @@ -825,7 +1004,7 @@ mod test_128bit_integers { Python::attach(|py| { let obj = py.eval(c"0", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance_of::(py)); + assert!(err.is_instance_of::(py)); }) } @@ -834,7 +1013,7 @@ mod test_128bit_integers { Python::attach(|py| { let obj = py.eval(c"0", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance_of::(py)); + assert!(err.is_instance_of::(py)); }) } }