From a087b0a52e46c84cab7d1ed6aadd653be1d3a9d6 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 2 Jul 2024 17:42:36 +0200 Subject: [PATCH] gh-102471: Add PyLong import and export API Add PyLong_Export() and PyLong_Import() functions and PyLong_LAYOUT structure. --- Doc/c-api/long.rst | 108 +++++++++++++ Doc/using/configure.rst | 3 +- Doc/whatsnew/3.14.rst | 8 + Include/cpython/longintrepr.h | 39 +++++ Lib/test/test_capi/test_long.py | 44 ++++++ ...-07-03-17-26-53.gh-issue-102471.XpmKYk.rst | 7 + Modules/_testcapi/long.c | 143 ++++++++++++++++++ Objects/longobject.c | 40 +++++ 8 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/C API/2024-07-03-17-26-53.gh-issue-102471.XpmKYk.rst diff --git a/Doc/c-api/long.rst b/Doc/c-api/long.rst index 42162914c0aec8..b990990647dd05 100644 --- a/Doc/c-api/long.rst +++ b/Doc/c-api/long.rst @@ -529,6 +529,9 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. Exactly what values are considered compact is an implementation detail and is subject to change. + .. versionadded:: 3.12 + + .. c:function:: Py_ssize_t PyUnstable_Long_CompactValue(const PyLongObject* op) If *op* is compact, as determined by :c:func:`PyUnstable_Long_IsCompact`, @@ -536,3 +539,108 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate. Otherwise, the return value is undefined. + .. versionadded:: 3.12 + + +Import/Export API +^^^^^^^^^^^^^^^^^ + +.. versionadded:: 3.14 + +.. c:type:: Py_digit + + A single unsigned digit. + + It is usually used in an *array of digits*, such as the + :c:member:`PyUnstable_LongExport.digits` array. + + Its size depend on the :c:macro:`PYLONG_BITS_IN_DIGIT` macro: + see the ``configure`` :option:`--enable-big-digits` option. + + See :c:member:`PyUnstable_Long_LAYOUT.bits_per_digit` for the number of bits per + digit and :c:member:`PyUnstable_Long_LAYOUT.digit_size` for the size of a digit (in + bytes). + + +.. c:struct:: PyUnstable_Long_LAYOUT + + Internal layout of a Python :class:`int` object. + + See also :attr:`sys.int_info` which exposes similar information to Python. + + .. c:member:: uint8_t bits_per_digit; + + Bits per digit. + + .. c:member:: uint8_t digit_size; + + Digit size in bytes. + + .. c:member:: int8_t word_endian; + + Word endian: + + - 1 for most significant byte first (big endian) + - 0 for least significant first (little endian) + + .. c:member:: int8_t array_endian; + + Array endian: + + - 1 for most significant byte first (big endian) + - 0 for least significant first (little endian) + + +.. c:function:: PyObject* PyUnstable_Long_Import(int negative, size_t ndigits, Py_digit *digits) + + Create a Python :class:`int` object from an array of digits. + + * Return a Python :class:`int` object on success. + * Set an exception and return ``NULL`` on error. + + *negative* is ``1`` if the number is negative, or ``0`` otherwise. + + *ndigits* is the number of digits in the *digits* array. + + *digits* is an array of unsigned digits. + + See :c:struct:`PyUnstable_Long_LAYOUT` for the internal layout of an integer. + + +.. c:struct:: PyUnstable_LongExport + + A Python :class:`int` object exported as an array of digits. + + See :c:struct:`PyUnstable_Long_LAYOUT` for the internal layout of an integer. + + .. c:member:: PyLongObject *obj + + Strong reference to the Python :class:`int` object. + + .. c:member:: int negative + + 1 if the number is negative, 0 otherwise. + + .. c:member:: size_t ndigits + + Number of digits in :c:member:`digits` array. + + .. c:member:: Py_digit *digits + + Array of unsigned digits. + + +.. c:function:: int PyUnstable_Long_Export(PyLongObject *obj, PyUnstable_LongExport *export) + + Export a Python :class:`int` object as an array of digits. + + * Set *\*export* and return 0 on success. + * Set an exception and return -1 on error. + + :c:func:`PyUnstable_Long_ReleaseExport` must be called once done with using + *export*. + + +.. c:function:: void PyUnstable_Long_ReleaseExport(PyUnstable_LongExport *export) + + Release an export created by :c:func:`PyUnstable_Long_Export`. diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst index 2a1f06e2d286ff..976787e6795f58 100644 --- a/Doc/using/configure.rst +++ b/Doc/using/configure.rst @@ -129,7 +129,8 @@ General Options Define the ``PYLONG_BITS_IN_DIGIT`` to ``15`` or ``30``. - See :data:`sys.int_info.bits_per_digit `. + See :data:`sys.int_info.bits_per_digit ` and the + :c:type:`Py_digit` type. .. option:: --with-suffix=SUFFIX diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 9578ba0c9c9657..00659a49687288 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -343,6 +343,14 @@ New Features (Contributed by Victor Stinner in :gh:`119182`.) +* Add a new unstable import and export API for Python :class:`int` objects: + + * :c:func:`PyUnstable_Long_Import`; + * :c:func:`PyUnstable_Long_Export`; + * :c:struct:`PyUnstable_Long_LAYOUT`. + + (Contributed by Victor Stinner in :gh:`102471`.) + Porting to Python 3.14 ---------------------- diff --git a/Include/cpython/longintrepr.h b/Include/cpython/longintrepr.h index d841c043f37fc4..7e11bee3cde654 100644 --- a/Include/cpython/longintrepr.h +++ b/Include/cpython/longintrepr.h @@ -61,6 +61,8 @@ typedef long stwodigits; /* signed variant of twodigits */ #define PyLong_BASE ((digit)1 << PyLong_SHIFT) #define PyLong_MASK ((digit)(PyLong_BASE - 1)) +typedef digit Py_digit; + /* Long integer representation. Long integers are made up of a number of 30- or 15-bit digits, depending on @@ -139,6 +141,43 @@ _PyLong_CompactValue(PyLongObject *op) #define PyUnstable_Long_CompactValue _PyLong_CompactValue +/* --- Import/Export API -------------------------------------------------- */ + +typedef struct PyUnstable_LongLayout { + // Bits per digit + uint8_t bits_per_digit; + + // Digit size in bytes: sizeof(digit) + uint8_t digit_size; + + // Word endian: + // - 1 for most significant byte first (big endian) + // - 0 for least significant first (little endian) + int8_t word_endian; + + // Array endian: + // - 1 for most significant byte first (big endian) + // - 0 for least significant first (little endian) + int8_t array_endian; +} PyUnstable_LongLayout; + +PyAPI_DATA(const PyUnstable_LongLayout) PyUnstable_Long_LAYOUT; + +PyAPI_FUNC(PyObject*) PyUnstable_Long_Import( + int negative, + size_t ndigits, + Py_digit *digits); + +typedef struct PyUnstable_LongExport { + PyLongObject *obj; + int negative; + size_t ndigits; + Py_digit *digits; +} PyUnstable_LongExport; + +PyAPI_FUNC(int) PyUnstable_Long_Export(PyLongObject *obj, PyUnstable_LongExport *export); +PyAPI_FUNC(void) PyUnstable_Long_ReleaseExport(PyUnstable_LongExport *export); + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_capi/test_long.py b/Lib/test/test_capi/test_long.py index 7e8d571ae234d1..4bd83a81bd22a2 100644 --- a/Lib/test/test_capi/test_long.py +++ b/Lib/test/test_capi/test_long.py @@ -744,6 +744,50 @@ def test_long_getsign(self): # CRASHES getsign(NULL) + def test_long_layout(self): + # Test PyLong_LAYOUT + int_info = sys.int_info + layout = _testcapi.get_pylong_layout() + expected = { + 'array_endian': 0, + 'bits_per_digit': int_info.bits_per_digit, + 'digit_size': int_info.sizeof_digit, + 'word_endian': 1 if sys.byteorder == 'little' else 0, + } + self.assertEqual(layout, expected) + + def test_long_export(self): + # Test PyLong_Export() + layout = _testcapi.get_pylong_layout() + shift = 2 ** layout['bits_per_digit'] + + pylong_export = _testcapi.pylong_export + self.assertEqual(pylong_export(0), (0, [0])) + self.assertEqual(pylong_export(123), (0, [123])) + self.assertEqual(pylong_export(-123), (1, [123])) + self.assertEqual(pylong_export(shift**2 * 3 + shift * 2 + 1), + (0, [1, 2, 3])) + + def test_long_import(self): + # Test PyLong_Import() + layout = _testcapi.get_pylong_layout() + shift = 2 ** layout['bits_per_digit'] + + pylong_import = _testcapi.pylong_import + self.assertEqual(pylong_import(0, [0]), 0) + self.assertEqual(pylong_import(0, [123]), 123) + self.assertEqual(pylong_import(1, [123]), -123) + self.assertEqual(pylong_import(0, [1, 2, 3]), + shift**2 * 3 + shift * 2 + 1) + + # round trip: Python int -> export -> Python int + pylong_export = _testcapi.pylong_export + numbers = [*range(0, 10), 12345, 0xdeadbeef, 2**100, 2**100-1] + numbers.extend(-num for num in list(numbers)) + for num in numbers: + with self.subTest(num=num): + export = pylong_export(num) + self.assertEqual(pylong_import(*export), num, export) if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C API/2024-07-03-17-26-53.gh-issue-102471.XpmKYk.rst b/Misc/NEWS.d/next/C API/2024-07-03-17-26-53.gh-issue-102471.XpmKYk.rst new file mode 100644 index 00000000000000..e5439e2c922a1f --- /dev/null +++ b/Misc/NEWS.d/next/C API/2024-07-03-17-26-53.gh-issue-102471.XpmKYk.rst @@ -0,0 +1,7 @@ +Add a new unstable import and export API for Python :class:`int` objects: + +* :c:func:`PyUnstable_Long_Import`; +* :c:func:`PyUnstable_Long_Export`; +* :c:struct:`PyUnstable_Long_LAYOUT`. + +Patch by Victor Stinner. diff --git a/Modules/_testcapi/long.c b/Modules/_testcapi/long.c index 2b5e85d5707522..4aa78f9a15af06 100644 --- a/Modules/_testcapi/long.c +++ b/Modules/_testcapi/long.c @@ -117,6 +117,146 @@ pylong_aspid(PyObject *module, PyObject *arg) } +static PyObject * +pylong_import(PyObject *module, PyObject *args) +{ + int negative; + PyObject *list; + if (!PyArg_ParseTuple(args, "iO!", &negative, &PyList_Type, &list)) { + return NULL; + } + Py_ssize_t ndigits = PyList_GET_SIZE(list); + + Py_digit *digits = PyMem_Malloc(ndigits * sizeof(Py_digit)); + if (digits == NULL) { + PyErr_NoMemory(); + return NULL; + } + + for (Py_ssize_t i=0; i < ndigits; i++) { + PyObject *item = PyList_GET_ITEM(list, i); + + long as_long = PyLong_AsLong(item); + if (as_long == -1 && PyErr_Occurred()) { + goto error; + } + + Py_digit digit = (Py_digit)as_long; + if ((long)digit != as_long) { + PyErr_SetString(PyExc_ValueError, "digit doesn't fit into Py_digit"); + goto error; + } + digits[i] = digit; + } + + PyObject *res = PyUnstable_Long_Import(negative, ndigits, digits); + PyMem_Free(digits); + + return res; + +error: + PyMem_Free(digits); + return NULL; +} + + +static PyObject * +pylong_export(PyObject *module, PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expect int, got %T", obj); + return NULL; + } + + PyUnstable_LongExport export; + if (PyUnstable_Long_Export((PyLongObject*)obj, &export) < 0) { + return NULL; + } + + PyObject *digits = PyList_New(0); + for (size_t i=0; i < export.ndigits; i++) { + PyObject *digit = PyLong_FromUnsignedLong(export.digits[i]); + if (digit == NULL) { + Py_DECREF(digits); + goto error; + } + + if (PyList_Append(digits, digit) < 0) { + Py_DECREF(digits); + Py_DECREF(digit); + goto error; + } + Py_DECREF(digit); + } + + PyObject *res = Py_BuildValue("(iN)", export.negative, digits); + PyUnstable_Long_ReleaseExport(&export); + return res; + +error: + PyUnstable_Long_ReleaseExport(&export); + return NULL; +} + + +static PyObject * +get_pylong_layout(PyObject *module, PyObject *Py_UNUSED(args)) +{ + PyUnstable_LongLayout layout = PyUnstable_Long_LAYOUT; + + PyObject *dict = PyDict_New(); + if (dict == NULL) { + goto error; + } + + PyObject *value = PyLong_FromUnsignedLong(layout.bits_per_digit); + if (value == NULL) { + goto error; + } + int res = PyDict_SetItemString(dict, "bits_per_digit", value); + Py_DECREF(value); + if (res < 0) { + goto error; + } + + value = PyLong_FromUnsignedLong(layout.digit_size); + if (value == NULL) { + goto error; + } + res = PyDict_SetItemString(dict, "digit_size", value); + Py_DECREF(value); + if (res < 0) { + goto error; + } + + value = PyLong_FromLong(layout.word_endian); + if (value == NULL) { + goto error; + } + res = PyDict_SetItemString(dict, "word_endian", value); + Py_DECREF(value); + if (res < 0) { + goto error; + } + + value = PyLong_FromLong(layout.array_endian); + if (value == NULL) { + goto error; + } + res = PyDict_SetItemString(dict, "array_endian", value); + Py_DECREF(value); + if (res < 0) { + goto error; + } + + return dict; + +error: + Py_XDECREF(dict); + return NULL; +} + + static PyMethodDef test_methods[] = { _TESTCAPI_CALL_LONG_COMPACT_API_METHODDEF {"pylong_fromunicodeobject", pylong_fromunicodeobject, METH_VARARGS}, @@ -124,6 +264,9 @@ static PyMethodDef test_methods[] = { {"pylong_fromnativebytes", pylong_fromnativebytes, METH_VARARGS}, {"pylong_getsign", pylong_getsign, METH_O}, {"pylong_aspid", pylong_aspid, METH_O}, + {"pylong_import", pylong_import, METH_VARARGS}, + {"pylong_export", pylong_export, METH_O}, + {"get_pylong_layout", get_pylong_layout, METH_NOARGS}, {NULL}, }; diff --git a/Objects/longobject.c b/Objects/longobject.c index 4ca259fb08e8c7..1deb3f91393af6 100644 --- a/Objects/longobject.c +++ b/Objects/longobject.c @@ -6685,3 +6685,43 @@ Py_ssize_t PyUnstable_Long_CompactValue(const PyLongObject* op) { return _PyLong_CompactValue((PyLongObject*)op); } + +const PyUnstable_LongLayout PyUnstable_Long_LAYOUT = { + .bits_per_digit = PyLong_SHIFT, + .word_endian = PY_LITTLE_ENDIAN, + .array_endian = 0, // least significant first + .digit_size = sizeof(digit), +}; + + +PyObject* +PyUnstable_Long_Import(int negative, size_t ndigits, Py_digit *digits) +{ + return (PyObject*)_PyLong_FromDigits(negative, ndigits, digits); +} + + +int +PyUnstable_Long_Export(PyLongObject *obj, PyUnstable_LongExport *export) +{ + assert(PyLong_Check(obj)); + + export->obj = (PyLongObject*)Py_NewRef(obj); + export->negative = _PyLong_IsNegative(obj); + export->ndigits = _PyLong_DigitCount(obj); + if (export->ndigits == 0) { + export->ndigits = 1; + } + export->digits = obj->long_value.ob_digit; + return 0; +} + + +void +PyUnstable_Long_ReleaseExport(PyUnstable_LongExport *export) +{ + Py_CLEAR(export->obj); + export->negative = 0; + export->ndigits = 0; + export->digits = NULL; +}