Skip to content

Commit a087b0a

Browse files
committed
gh-102471: Add PyLong import and export API
Add PyLong_Export() and PyLong_Import() functions and PyLong_LAYOUT structure.
1 parent 7c66906 commit a087b0a

File tree

8 files changed

+391
-1
lines changed

8 files changed

+391
-1
lines changed

Doc/c-api/long.rst

+108
Original file line numberDiff line numberDiff line change
@@ -529,10 +529,118 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
529529
Exactly what values are considered compact is an implementation detail
530530
and is subject to change.
531531
532+
.. versionadded:: 3.12
533+
534+
532535
.. c:function:: Py_ssize_t PyUnstable_Long_CompactValue(const PyLongObject* op)
533536
534537
If *op* is compact, as determined by :c:func:`PyUnstable_Long_IsCompact`,
535538
return its value.
536539
537540
Otherwise, the return value is undefined.
538541
542+
.. versionadded:: 3.12
543+
544+
545+
Import/Export API
546+
^^^^^^^^^^^^^^^^^
547+
548+
.. versionadded:: 3.14
549+
550+
.. c:type:: Py_digit
551+
552+
A single unsigned digit.
553+
554+
It is usually used in an *array of digits*, such as the
555+
:c:member:`PyUnstable_LongExport.digits` array.
556+
557+
Its size depend on the :c:macro:`PYLONG_BITS_IN_DIGIT` macro:
558+
see the ``configure`` :option:`--enable-big-digits` option.
559+
560+
See :c:member:`PyUnstable_Long_LAYOUT.bits_per_digit` for the number of bits per
561+
digit and :c:member:`PyUnstable_Long_LAYOUT.digit_size` for the size of a digit (in
562+
bytes).
563+
564+
565+
.. c:struct:: PyUnstable_Long_LAYOUT
566+
567+
Internal layout of a Python :class:`int` object.
568+
569+
See also :attr:`sys.int_info` which exposes similar information to Python.
570+
571+
.. c:member:: uint8_t bits_per_digit;
572+
573+
Bits per digit.
574+
575+
.. c:member:: uint8_t digit_size;
576+
577+
Digit size in bytes.
578+
579+
.. c:member:: int8_t word_endian;
580+
581+
Word endian:
582+
583+
- 1 for most significant byte first (big endian)
584+
- 0 for least significant first (little endian)
585+
586+
.. c:member:: int8_t array_endian;
587+
588+
Array endian:
589+
590+
- 1 for most significant byte first (big endian)
591+
- 0 for least significant first (little endian)
592+
593+
594+
.. c:function:: PyObject* PyUnstable_Long_Import(int negative, size_t ndigits, Py_digit *digits)
595+
596+
Create a Python :class:`int` object from an array of digits.
597+
598+
* Return a Python :class:`int` object on success.
599+
* Set an exception and return ``NULL`` on error.
600+
601+
*negative* is ``1`` if the number is negative, or ``0`` otherwise.
602+
603+
*ndigits* is the number of digits in the *digits* array.
604+
605+
*digits* is an array of unsigned digits.
606+
607+
See :c:struct:`PyUnstable_Long_LAYOUT` for the internal layout of an integer.
608+
609+
610+
.. c:struct:: PyUnstable_LongExport
611+
612+
A Python :class:`int` object exported as an array of digits.
613+
614+
See :c:struct:`PyUnstable_Long_LAYOUT` for the internal layout of an integer.
615+
616+
.. c:member:: PyLongObject *obj
617+
618+
Strong reference to the Python :class:`int` object.
619+
620+
.. c:member:: int negative
621+
622+
1 if the number is negative, 0 otherwise.
623+
624+
.. c:member:: size_t ndigits
625+
626+
Number of digits in :c:member:`digits` array.
627+
628+
.. c:member:: Py_digit *digits
629+
630+
Array of unsigned digits.
631+
632+
633+
.. c:function:: int PyUnstable_Long_Export(PyLongObject *obj, PyUnstable_LongExport *export)
634+
635+
Export a Python :class:`int` object as an array of digits.
636+
637+
* Set *\*export* and return 0 on success.
638+
* Set an exception and return -1 on error.
639+
640+
:c:func:`PyUnstable_Long_ReleaseExport` must be called once done with using
641+
*export*.
642+
643+
644+
.. c:function:: void PyUnstable_Long_ReleaseExport(PyUnstable_LongExport *export)
645+
646+
Release an export created by :c:func:`PyUnstable_Long_Export`.

Doc/using/configure.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ General Options
129129

130130
Define the ``PYLONG_BITS_IN_DIGIT`` to ``15`` or ``30``.
131131

132-
See :data:`sys.int_info.bits_per_digit <sys.int_info>`.
132+
See :data:`sys.int_info.bits_per_digit <sys.int_info>` and the
133+
:c:type:`Py_digit` type.
133134

134135
.. option:: --with-suffix=SUFFIX
135136

Doc/whatsnew/3.14.rst

+8
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,14 @@ New Features
343343

344344
(Contributed by Victor Stinner in :gh:`119182`.)
345345

346+
* Add a new unstable import and export API for Python :class:`int` objects:
347+
348+
* :c:func:`PyUnstable_Long_Import`;
349+
* :c:func:`PyUnstable_Long_Export`;
350+
* :c:struct:`PyUnstable_Long_LAYOUT`.
351+
352+
(Contributed by Victor Stinner in :gh:`102471`.)
353+
346354
Porting to Python 3.14
347355
----------------------
348356

Include/cpython/longintrepr.h

+39
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ typedef long stwodigits; /* signed variant of twodigits */
6161
#define PyLong_BASE ((digit)1 << PyLong_SHIFT)
6262
#define PyLong_MASK ((digit)(PyLong_BASE - 1))
6363

64+
typedef digit Py_digit;
65+
6466
/* Long integer representation.
6567
6668
Long integers are made up of a number of 30- or 15-bit digits, depending on
@@ -139,6 +141,43 @@ _PyLong_CompactValue(PyLongObject *op)
139141
#define PyUnstable_Long_CompactValue _PyLong_CompactValue
140142

141143

144+
/* --- Import/Export API -------------------------------------------------- */
145+
146+
typedef struct PyUnstable_LongLayout {
147+
// Bits per digit
148+
uint8_t bits_per_digit;
149+
150+
// Digit size in bytes: sizeof(digit)
151+
uint8_t digit_size;
152+
153+
// Word endian:
154+
// - 1 for most significant byte first (big endian)
155+
// - 0 for least significant first (little endian)
156+
int8_t word_endian;
157+
158+
// Array endian:
159+
// - 1 for most significant byte first (big endian)
160+
// - 0 for least significant first (little endian)
161+
int8_t array_endian;
162+
} PyUnstable_LongLayout;
163+
164+
PyAPI_DATA(const PyUnstable_LongLayout) PyUnstable_Long_LAYOUT;
165+
166+
PyAPI_FUNC(PyObject*) PyUnstable_Long_Import(
167+
int negative,
168+
size_t ndigits,
169+
Py_digit *digits);
170+
171+
typedef struct PyUnstable_LongExport {
172+
PyLongObject *obj;
173+
int negative;
174+
size_t ndigits;
175+
Py_digit *digits;
176+
} PyUnstable_LongExport;
177+
178+
PyAPI_FUNC(int) PyUnstable_Long_Export(PyLongObject *obj, PyUnstable_LongExport *export);
179+
PyAPI_FUNC(void) PyUnstable_Long_ReleaseExport(PyUnstable_LongExport *export);
180+
142181
#ifdef __cplusplus
143182
}
144183
#endif

Lib/test/test_capi/test_long.py

+44
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,50 @@ def test_long_getsign(self):
744744

745745
# CRASHES getsign(NULL)
746746

747+
def test_long_layout(self):
748+
# Test PyLong_LAYOUT
749+
int_info = sys.int_info
750+
layout = _testcapi.get_pylong_layout()
751+
expected = {
752+
'array_endian': 0,
753+
'bits_per_digit': int_info.bits_per_digit,
754+
'digit_size': int_info.sizeof_digit,
755+
'word_endian': 1 if sys.byteorder == 'little' else 0,
756+
}
757+
self.assertEqual(layout, expected)
758+
759+
def test_long_export(self):
760+
# Test PyLong_Export()
761+
layout = _testcapi.get_pylong_layout()
762+
shift = 2 ** layout['bits_per_digit']
763+
764+
pylong_export = _testcapi.pylong_export
765+
self.assertEqual(pylong_export(0), (0, [0]))
766+
self.assertEqual(pylong_export(123), (0, [123]))
767+
self.assertEqual(pylong_export(-123), (1, [123]))
768+
self.assertEqual(pylong_export(shift**2 * 3 + shift * 2 + 1),
769+
(0, [1, 2, 3]))
770+
771+
def test_long_import(self):
772+
# Test PyLong_Import()
773+
layout = _testcapi.get_pylong_layout()
774+
shift = 2 ** layout['bits_per_digit']
775+
776+
pylong_import = _testcapi.pylong_import
777+
self.assertEqual(pylong_import(0, [0]), 0)
778+
self.assertEqual(pylong_import(0, [123]), 123)
779+
self.assertEqual(pylong_import(1, [123]), -123)
780+
self.assertEqual(pylong_import(0, [1, 2, 3]),
781+
shift**2 * 3 + shift * 2 + 1)
782+
783+
# round trip: Python int -> export -> Python int
784+
pylong_export = _testcapi.pylong_export
785+
numbers = [*range(0, 10), 12345, 0xdeadbeef, 2**100, 2**100-1]
786+
numbers.extend(-num for num in list(numbers))
787+
for num in numbers:
788+
with self.subTest(num=num):
789+
export = pylong_export(num)
790+
self.assertEqual(pylong_import(*export), num, export)
747791

748792
if __name__ == "__main__":
749793
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Add a new unstable import and export API for Python :class:`int` objects:
2+
3+
* :c:func:`PyUnstable_Long_Import`;
4+
* :c:func:`PyUnstable_Long_Export`;
5+
* :c:struct:`PyUnstable_Long_LAYOUT`.
6+
7+
Patch by Victor Stinner.

0 commit comments

Comments
 (0)