Skip to content

Commit f4fdbf2

Browse files
vstinnerskirpichev
andcommitted
pythongh-102471: Add PyLong import and export API
Co-authored-by: Sergey B Kirpichev <[email protected]>
1 parent 4c31791 commit f4fdbf2

File tree

10 files changed

+512
-3
lines changed

10 files changed

+512
-3
lines changed

Doc/c-api/long.rst

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,163 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
540540
Exactly what values are considered compact is an implementation detail
541541
and is subject to change.
542542
543+
.. versionadded:: 3.12
544+
545+
543546
.. c:function:: Py_ssize_t PyUnstable_Long_CompactValue(const PyLongObject* op)
544547
545548
If *op* is compact, as determined by :c:func:`PyUnstable_Long_IsCompact`,
546549
return its value.
547550
548551
Otherwise, the return value is undefined.
549552
553+
.. versionadded:: 3.12
554+
555+
556+
Export API
557+
^^^^^^^^^^
558+
559+
.. versionadded:: 3.14
560+
561+
.. c:type:: Py_digit
562+
563+
A single unsigned digit in the range [``0``; ``PyLong_BASE - 1``].
564+
565+
It is usually used in an *array of digits*, such as the
566+
:c:member:`PyLong_DigitArray.digits` array.
567+
568+
Its size depend on the :c:macro:`!PYLONG_BITS_IN_DIGIT` macro:
569+
see the ``configure`` :option:`--enable-big-digits` option.
570+
571+
See :c:member:`PyLong_LAYOUT.bits_per_digit` for the number of bits per
572+
digit and :c:member:`PyLong_LAYOUT.digit_size` for the size of a digit (in
573+
bytes).
574+
575+
576+
.. c:struct:: PyLong_LAYOUT
577+
578+
Layout of an array of digits, used by Python :class:`int` object.
579+
580+
See also :attr:`sys.int_info` which exposes similar information to Python.
581+
582+
.. c:member:: uint8_t bits_per_digit
583+
584+
Bits per digit.
585+
586+
.. c:member:: uint8_t digit_size
587+
588+
Digit size in bytes.
589+
590+
.. c:member:: int8_t word_endian
591+
592+
Word endian:
593+
594+
- ``1`` for most significant word first (big endian)
595+
- ``-1`` for least significant first (little endian)
596+
597+
.. c:member:: int8_t array_endian
598+
599+
Array endian:
600+
601+
- ``1`` for most significant byte first (big endian)
602+
- ``-1`` for least significant first (little endian)
603+
604+
605+
.. c:struct:: PyLong_DigitArray
606+
607+
A Python :class:`int` object exported as an array of digits.
608+
609+
See :c:struct:`PyLong_LAYOUT` for the :c:member:`digits` layout.
610+
611+
.. c:member:: PyObject *obj
612+
613+
Strong reference to the Python :class:`int` object.
614+
615+
.. c:member:: int negative
616+
617+
1 if the number is negative, 0 otherwise.
618+
619+
.. c:member:: Py_ssize_t ndigits
620+
621+
Number of digits in :c:member:`digits` array.
622+
623+
.. c:member:: const Py_digit *digits
624+
625+
Read-only array of unsigned digits.
626+
627+
628+
.. c:function:: int PyLong_AsDigitArray(PyObject *obj, PyLong_DigitArray *array)
629+
630+
Export a Python :class:`int` object as an array of digits.
631+
632+
On success, set *\*array* and return 0.
633+
On error, set an exception and return -1.
634+
635+
This function always succeeds if *obj* is a Python :class:`int` object or a
636+
subclass.
637+
638+
:c:func:`PyLong_FreeDigitArray` must be called once done with using
639+
*export*.
640+
641+
642+
.. c:function:: void PyLong_FreeDigitArray(PyLong_DigitArray *array)
643+
644+
Release the export *array* created by :c:func:`PyLong_AsDigitArray`.
645+
646+
647+
PyLongWriter API
648+
^^^^^^^^^^^^^^^^
649+
650+
The :c:type:`PyLongWriter` API can be used to import an integer.
651+
652+
.. versionadded:: 3.14
653+
654+
.. c:struct:: PyLongWriter
655+
656+
A Python :class:`int` writer instance.
657+
658+
The instance must be destroyed by :c:func:`PyLongWriter_Finish`.
659+
660+
661+
.. c:function:: PyLongWriter* PyLongWriter_Create(int negative, Py_ssize_t ndigits, Py_digit **digits)
662+
663+
Create a :c:type:`PyLongWriter`.
664+
665+
On success, set *\*digits* and return a writer.
666+
On error, set an exception and return ``NULL``.
667+
668+
*negative* is ``1`` if the number is negative, or ``0`` otherwise.
669+
670+
*ndigits* is the number of digits in the *digits* array. It must be
671+
positive.
672+
673+
The caller must initialize the array of digits *digits* and then call
674+
:c:func:`PyLongWriter_Finish` to get a Python :class:`int`. Digits must be
675+
in the range [``0``; ``PyLong_BASE - 1``]. Unused digits must be set to
676+
``0``.
677+
678+
See :c:struct:`PyLong_LAYOUT` for the layout of an array of digits.
679+
680+
681+
.. c:function:: PyObject* PyLongWriter_Finish(PyLongWriter *writer)
682+
683+
Finish a :c:type:`PyLongWriter` created by :c:func:`PyLongWriter_Create`.
684+
685+
On success, return a Python :class:`int` object.
686+
On error, set an exception and return ``NULL``.
687+
688+
689+
Example creating an integer from an array of digits::
690+
691+
PyObject *
692+
long_import(int negative, Py_ssize_t ndigits, Py_digit *digits)
693+
{
694+
Py_digit *writer_digits;
695+
PyLongWriter *writer = PyLongWriter_Create(negative, ndigits,
696+
&writer_digits);
697+
if (writer == NULL) {
698+
return NULL;
699+
}
700+
memcpy(writer_digits, digits, ndigits * sizeof(digit));
701+
return PyLongWriter_Finish(writer);
702+
}

Doc/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@
141141
('c:type', 'size_t'),
142142
('c:type', 'ssize_t'),
143143
('c:type', 'time_t'),
144+
('c:type', 'int8_t'),
145+
('c:type', 'uint8_t'),
144146
('c:type', 'uint64_t'),
145147
('c:type', 'uintmax_t'),
146148
('c:type', 'uintptr_t'),

Doc/using/configure.rst

Lines changed: 2 additions & 1 deletion
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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,16 @@ New Features
405405

406406
(Contributed by Victor Stinner in :gh:`119182`.)
407407

408+
* Add a new import and export API for Python :class:`int` objects:
409+
410+
* :c:func:`PyLong_AsDigitArray`;
411+
* :c:func:`PyLong_FreeDigitArray`;
412+
* :c:func:`PyLongWriter_Create`;
413+
* :c:func:`PyLongWriter_Finish`;
414+
* :c:struct:`PyLong_LAYOUT`.
415+
416+
(Contributed by Victor Stinner in :gh:`102471`.)
417+
408418
Porting to Python 3.14
409419
----------------------
410420

Include/cpython/longintrepr.h

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ typedef long stwodigits; /* signed variant of twodigits */
5858
#else
5959
#error "PYLONG_BITS_IN_DIGIT should be 15 or 30"
6060
#endif
61-
#define PyLong_BASE ((digit)1 << PyLong_SHIFT)
62-
#define PyLong_MASK ((digit)(PyLong_BASE - 1))
61+
#define PyLong_BASE ((Py_digit)1 << PyLong_SHIFT)
62+
#define PyLong_MASK ((Py_digit)(PyLong_BASE - 1))
63+
64+
typedef digit Py_digit;
6365

6466
/* Long integer representation.
6567
@@ -139,6 +141,52 @@ _PyLong_CompactValue(const PyLongObject *op)
139141
#define PyUnstable_Long_CompactValue _PyLong_CompactValue
140142

141143

144+
/* --- Import/Export API -------------------------------------------------- */
145+
146+
typedef struct PyLongLayout {
147+
// Bits per digit
148+
uint8_t bits_per_digit;
149+
150+
// Digit size in bytes
151+
uint8_t digit_size;
152+
153+
// Word endian:
154+
// * 1 for most significant word first (big endian)
155+
// * -1 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+
// * -1 for least significant first (little endian)
161+
int8_t array_endian;
162+
} PyLongLayout;
163+
164+
PyAPI_DATA(const PyLongLayout) PyLong_LAYOUT;
165+
166+
typedef struct PyLong_DigitArray {
167+
PyObject *obj;
168+
int negative;
169+
Py_ssize_t ndigits;
170+
const Py_digit *digits;
171+
} PyLong_DigitArray;
172+
173+
PyAPI_FUNC(int) PyLong_AsDigitArray(
174+
PyObject *obj,
175+
PyLong_DigitArray *array);
176+
PyAPI_FUNC(void) PyLong_FreeDigitArray(
177+
PyLong_DigitArray *array);
178+
179+
180+
/* --- PyLongWriter API --------------------------------------------------- */
181+
182+
typedef struct PyLongWriter PyLongWriter;
183+
184+
PyAPI_FUNC(PyLongWriter*) PyLongWriter_Create(
185+
int negative,
186+
Py_ssize_t ndigits,
187+
Py_digit **digits);
188+
PyAPI_FUNC(PyObject*) PyLongWriter_Finish(PyLongWriter *writer);
189+
142190
#ifdef __cplusplus
143191
}
144192
#endif

Lib/test/test_capi/test_long.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,72 @@ def test_long_getsign(self):
631631

632632
# CRASHES getsign(NULL)
633633

634+
def test_long_layout(self):
635+
# Test PyLong_LAYOUT
636+
int_info = sys.int_info
637+
layout = _testcapi.get_pylong_layout()
638+
expected = {
639+
'array_endian': -1,
640+
'bits_per_digit': int_info.bits_per_digit,
641+
'digit_size': int_info.sizeof_digit,
642+
'word_endian': -1 if sys.byteorder == 'little' else 1,
643+
}
644+
self.assertEqual(layout, expected)
645+
646+
def test_long_export(self):
647+
# Test PyLong_Export()
648+
layout = _testcapi.get_pylong_layout()
649+
base = 2 ** layout['bits_per_digit']
650+
651+
pylong_export = _testcapi.pylong_export
652+
self.assertEqual(pylong_export(0), (0, [0]))
653+
self.assertEqual(pylong_export(123), (0, [123]))
654+
self.assertEqual(pylong_export(-123), (1, [123]))
655+
self.assertEqual(pylong_export(base**2 * 3 + base * 2 + 1),
656+
(0, [1, 2, 3]))
657+
658+
with self.assertRaises(TypeError):
659+
pylong_export(1.0)
660+
with self.assertRaises(TypeError):
661+
pylong_export(0+1j)
662+
with self.assertRaises(TypeError):
663+
pylong_export("abc")
664+
665+
def test_longwriter_create(self):
666+
# Test PyLong_Import()
667+
layout = _testcapi.get_pylong_layout()
668+
base = 2 ** layout['bits_per_digit']
669+
670+
pylongwriter_create = _testcapi.pylongwriter_create
671+
self.assertEqual(pylongwriter_create(0, []), 0)
672+
self.assertEqual(pylongwriter_create(0, [0]), 0)
673+
self.assertEqual(pylongwriter_create(0, [123]), 123)
674+
self.assertEqual(pylongwriter_create(1, [123]), -123)
675+
self.assertEqual(pylongwriter_create(1, [1, 2]),
676+
-(base * 2 + 1))
677+
self.assertEqual(pylongwriter_create(0, [1, 2, 3]),
678+
base**2 * 3 + base * 2 + 1)
679+
max_digit = base - 1
680+
self.assertEqual(pylongwriter_create(0, [max_digit, max_digit, max_digit]),
681+
base**2 * max_digit + base * max_digit + max_digit)
682+
683+
# normalize
684+
self.assertEqual(pylongwriter_create(0, [123, 0, 0]), 123)
685+
686+
# test singletons + normalize
687+
for num in (-2, 0, 1, 5, 42, 100):
688+
self.assertIs(pylongwriter_create(bool(num < 0), [abs(num), 0]),
689+
num)
690+
691+
# round trip: Python int -> export -> Python int
692+
pylong_export = _testcapi.pylong_export
693+
numbers = [*range(0, 10), 12345, 0xdeadbeef, 2**100, 2**100-1]
694+
numbers.extend(-num for num in list(numbers))
695+
for num in numbers:
696+
with self.subTest(num=num):
697+
export = pylong_export(num)
698+
self.assertEqual(pylongwriter_create(*export), num, export)
699+
634700

635701
if __name__ == "__main__":
636702
unittest.main()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Add a new import and export API for Python :class:`int` objects:
2+
3+
* :c:func:`PyLong_AsDigitArray`;
4+
* :c:func:`PyLong_FreeDigitArray`;
5+
* :c:func:`PyLongWriter_Create`;
6+
* :c:func:`PyLongWriter_Finish`;
7+
* :c:struct:`PyLong_LAYOUT`.
8+
9+
Patch by Victor Stinner.

0 commit comments

Comments
 (0)