Skip to content

Commit

Permalink
pythongh-115754: Add Py_GetConstant() function
Browse files Browse the repository at this point in the history
Add Py_GetConstant() and Py_GetConstantBorrowed() functions.

In the limited C API version 3.13, getting Py_None, Py_False,
Py_True, Py_Ellipsis and Py_NotImplemented singletons is now
implemented as function calls at the stable ABI level to hide
implementation details. Getting these constants still return borrowed
references.

Add _testlimitedcapi/object.c and test_capi/test_object.py to test
Py_GetConstant() and Py_GetConstantBorrowed() functions.
  • Loading branch information
vstinner committed Mar 19, 2024
1 parent a3cf0fa commit 2481445
Show file tree
Hide file tree
Showing 22 changed files with 295 additions and 6 deletions.
27 changes: 27 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ Object Protocol
===============


.. c:function:: PyObject* Py_GetConstant(unsigned int constant_id)
Get a :term:`strong reference` to a constant.
Return ``NULL`` if *constant_id* is invalid.
Constant identifiers:
.. c:macro: Py_NONE_IDX
.. c:macro: Py_FALSE_IDX
.. c:macro: Py_TRUE_IDX
.. c:macro: Py_ELLIPSIS_IDX
.. c:macro: Py_NOT_IMPLEMENTED_IDX
.. c:macro: Py_ZERO_IDX
.. c:macro: Py_ONE_IDX
.. c:macro: Py_EMPTY_STR_IDX
.. c:macro: Py_EMPTY_BYTES_IDX
.. c:macro: Py_EMPTY_TUPLE_IDX
.. versionadded:: 3.13
.. c:function:: PyObject* Py_GetConstantBorrowed(unsigned int constant_id)
Similar to :c:func:`Py_GetConstant`, but return a :term:`borrowed
reference`.
.. c:var:: PyObject* Py_NotImplemented
The ``NotImplemented`` singleton, used to signal that an operation is
Expand Down
2 changes: 2 additions & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,17 @@ New Features
more information.
(Contributed by Victor Stinner in :gh:`111696`.)

* Add :c:func:`Py_GetConstant` and :c:func:`Py_GetConstantBorrowed` functions
to get constants. For example, ``Py_GetConstant(Py_ZERO_IDX)`` returns a
:term:`strong reference` to the constant zero.
(Contributed by Victor Stinner in :gh:`115754`.)

* In the limited C API version 3.13, getting ``Py_None``, ``Py_False``,
``Py_True``, ``Py_Ellipsis`` and ``Py_NotImplemented`` singletons is now
implemented as function calls at the stable ABI level to hide implementation
details. Getting these constants still return borrowed references.
(Contributed by Victor Stinner in :gh:`115754`.)


Porting to Python 3.13
----------------------
Expand Down
9 changes: 7 additions & 2 deletions Include/boolobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ PyAPI_DATA(PyLongObject) _Py_FalseStruct;
PyAPI_DATA(PyLongObject) _Py_TrueStruct;

/* Use these macros */
#define Py_False _PyObject_CAST(&_Py_FalseStruct)
#define Py_True _PyObject_CAST(&_Py_TrueStruct)
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000
# define Py_False Py_GetConstantBorrowed(Py_FALSE_IDX)
# define Py_True Py_GetConstantBorrowed(Py_TRUE_IDX)
#else
# define Py_False _PyObject_CAST(&_Py_FalseStruct)
# define Py_True _PyObject_CAST(&_Py_TrueStruct)
#endif

// Test if an object is the True singleton, the same as "x is True" in Python.
PyAPI_FUNC(int) Py_IsTrue(PyObject *x);
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,8 @@ PyAPI_DATA(PyTypeObject) _PyNotImplemented_Type;
// Export for the stable ABI.
PyAPI_DATA(int) _Py_SwappedOp[];

extern void _Py_GetConstant_Init(void);

#ifdef __cplusplus
}
#endif
Expand Down
31 changes: 29 additions & 2 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -1068,12 +1068,34 @@ static inline PyObject* _Py_XNewRef(PyObject *obj)
#endif


#define Py_NONE_IDX 0
#define Py_FALSE_IDX 1
#define Py_TRUE_IDX 2
#define Py_ELLIPSIS_IDX 3
#define Py_NOT_IMPLEMENTED_IDX 4
#define Py_ZERO_IDX 5
#define Py_ONE_IDX 6
#define Py_EMPTY_STR_IDX 7
#define Py_EMPTY_BYTES_IDX 8
#define Py_EMPTY_TUPLE_IDX 9

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
PyAPI_FUNC(PyObject*) Py_GetConstant(unsigned int constant_id);
PyAPI_FUNC(PyObject*) Py_GetConstantBorrowed(unsigned int constant_id);
#endif


/*
_Py_NoneStruct is an object of undefined type which can be used in contexts
where NULL (nil) is not suitable (since NULL often means 'error').
*/
PyAPI_DATA(PyObject) _Py_NoneStruct; /* Don't use this directly */
#define Py_None (&_Py_NoneStruct)

#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000
# define Py_None Py_GetConstantBorrowed(Py_NONE_IDX)
#else
# define Py_None (&_Py_NoneStruct)
#endif

// Test if an object is the None singleton, the same as "x is None" in Python.
PyAPI_FUNC(int) Py_IsNone(PyObject *x);
Expand All @@ -1087,7 +1109,12 @@ Py_NotImplemented is a singleton used to signal that an operation is
not implemented for a given type combination.
*/
PyAPI_DATA(PyObject) _Py_NotImplementedStruct; /* Don't use this directly */
#define Py_NotImplemented (&_Py_NotImplementedStruct)

#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000
# define Py_NotImplemented Py_GetConstantBorrowed(Py_NOT_IMPLEMENTED_IDX)
#else
# define Py_NotImplemented (&_Py_NotImplementedStruct)
#endif

/* Macro for returning Py_NotImplemented from a function */
#define Py_RETURN_NOTIMPLEMENTED return Py_NotImplemented
Expand Down
6 changes: 5 additions & 1 deletion Include/sliceobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ extern "C" {

PyAPI_DATA(PyObject) _Py_EllipsisObject; /* Don't use this directly */

#define Py_Ellipsis (&_Py_EllipsisObject)
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000
# define Py_Ellipsis Py_GetConstantBorrowed(Py_ELLIPSIS_IDX)
#else
# define Py_Ellipsis (&_Py_EllipsisObject)
#endif

/* Slice object interface */

Expand Down
54 changes: 54 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import enum
import unittest
from test.support import import_helper

_testlimitedcapi = import_helper.import_module('_testlimitedcapi')


class Constant(enum.IntEnum):
Py_NONE_IDX = 0
Py_FALSE_IDX = 1
Py_TRUE_IDX = 2
Py_ELLIPSIS_IDX = 3
Py_NOT_IMPLEMENTED_IDX = 4
Py_ZERO_IDX = 5
Py_ONE_IDX = 6
Py_EMPTY_STR_IDX = 7
Py_EMPTY_BYTES_IDX = 8
Py_EMPTY_TUPLE_IDX = 9

INVALID_IDX = Py_EMPTY_TUPLE_IDX + 1


class CAPITest(unittest.TestCase):
def check_get_constant(self, get_constant):
self.assertIs(get_constant(Constant.Py_NONE_IDX), None)
self.assertIs(get_constant(Constant.Py_FALSE_IDX), False)
self.assertIs(get_constant(Constant.Py_TRUE_IDX), True)
self.assertIs(get_constant(Constant.Py_ELLIPSIS_IDX), Ellipsis)
self.assertIs(get_constant(Constant.Py_NOT_IMPLEMENTED_IDX), NotImplemented)

for constant_id, constant_type, value in (
(Constant.Py_ZERO_IDX, int, 0),
(Constant.Py_ONE_IDX, int, 1),
(Constant.Py_EMPTY_STR_IDX, str, ""),
(Constant.Py_EMPTY_BYTES_IDX, bytes, b""),
(Constant.Py_EMPTY_TUPLE_IDX, tuple, ()),
):
with self.subTest(constant_id=constant_id):
obj = get_constant(constant_id)
self.assertEqual(type(obj), constant_type, obj)
self.assertEqual(obj, value)

with self.assertRaises(ValueError):
get_constant(Constant.INVALID_IDX)

def test_get_constant(self):
self.check_get_constant(_testlimitedcapi.get_constant)

def test_get_constant_borrowed(self):
self.check_get_constant(_testlimitedcapi.get_constant_borrowed)


if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`Py_GetConstant` and :c:func:`Py_GetConstantBorrowed` functions to
get constants. For example, ``Py_GetConstant(Py_ZERO_IDX)`` returns a
:term:`strong reference` to the constant zero. Patch by Victor Stinner.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
In the limited C API version 3.13, getting ``Py_None``, ``Py_False``,
``Py_True``, ``Py_Ellipsis`` and ``Py_NotImplemented`` singletons is now
implemented as function calls at the stable ABI level to hide implementation
details. Getting these constants still return borrowed references. Patch by
Victor Stinner.
4 changes: 4 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2500,3 +2500,7 @@
added = '3.13'
[function.PyType_GetModuleName]
added = '3.13'
[function.Py_GetConstant]
added = '3.13'
[function.Py_GetConstantBorrowed]
added = '3.13'
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/vectorcall_limited.c
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/vectorcall_limited.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Expand Down
3 changes: 3 additions & 0 deletions Modules/_testlimitedcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ PyInit__testlimitedcapi(void)
if (_PyTestLimitedCAPI_Init_List(mod) < 0) {
return NULL;
}
if (_PyTestLimitedCAPI_Init_Object(mod) < 0) {
return NULL;
}
if (_PyTestLimitedCAPI_Init_PyOS(mod) < 0) {
return NULL;
}
Expand Down
80 changes: 80 additions & 0 deletions Modules/_testlimitedcapi/object.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Need limited C API version 3.13 for Py_GetConstant()
#include "pyconfig.h" // Py_GIL_DISABLED
#if !defined(Py_GIL_DISABLED) && !defined(Py_LIMITED_API )
# define Py_LIMITED_API 0x030d0000
#endif

#include "parts.h"
#include "util.h"


/* Test Py_GetConstant() */
static PyObject *
get_constant(PyObject *Py_UNUSED(module), PyObject *args)
{
int constant_id;
if (!PyArg_ParseTuple(args, "i", &constant_id)) {
return NULL;
}

PyObject *obj = Py_GetConstant(constant_id);
if (obj == NULL) {
PyErr_SetString(PyExc_ValueError, "invalid constant identifier");
return NULL;
}
return obj;
}


/* Test Py_GetConstantBorrowed() */
static PyObject *
get_constant_borrowed(PyObject *Py_UNUSED(module), PyObject *args)
{
int constant_id;
if (!PyArg_ParseTuple(args, "i", &constant_id)) {
return NULL;
}

PyObject *obj = Py_GetConstantBorrowed(constant_id);
if (obj == NULL) {
PyErr_SetString(PyExc_ValueError, "invalid constant identifier");
return NULL;
}
return Py_NewRef(obj);
}


/* Test constants */
static PyObject *
test_constants(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
{
// Test that implementation of constants in the limited C API:
// check that the C code compiles.
//
// Test also that constants and Py_GetConstant() return the same
// objects.
assert(Py_None == Py_GetConstant(Py_NONE_IDX));
assert(Py_False == Py_GetConstant(Py_FALSE_IDX));
assert(Py_True == Py_GetConstant(Py_TRUE_IDX));
assert(Py_Ellipsis == Py_GetConstant(Py_ELLIPSIS_IDX));
assert(Py_NotImplemented == Py_GetConstant(Py_NOT_IMPLEMENTED_IDX));
// Other constants are tested in test_capi.test_object
Py_RETURN_NONE;
}

static PyMethodDef test_methods[] = {
{"get_constant", get_constant, METH_VARARGS},
{"get_constant_borrowed", get_constant_borrowed, METH_VARARGS},
{"test_constants", test_constants, METH_NOARGS},
{NULL},
};

int
_PyTestCapi_Init_Object(PyObject *m)
{
if (PyModule_AddFunctions(m, test_methods) < 0) {
return -1;
}

return 0;
}
1 change: 1 addition & 0 deletions Modules/_testlimitedcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
int _PyTestLimitedCAPI_Init_ByteArray(PyObject *module);
int _PyTestLimitedCAPI_Init_Bytes(PyObject *module);
int _PyTestLimitedCAPI_Init_HeaptypeRelative(PyObject *module);
int _PyTestLimitedCAPI_Init_Object(PyObject *module);
int _PyTestLimitedCAPI_Init_List(PyObject *module);
int _PyTestLimitedCAPI_Init_PyOS(PyObject *module);
int _PyTestLimitedCAPI_Init_Set(PyObject *module);
Expand Down
50 changes: 50 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "pycore_memoryobject.h" // _PyManagedBuffer_Type
#include "pycore_namespace.h" // _PyNamespace_Type
#include "pycore_object.h" // PyAPI_DATA() _Py_SwappedOp definition
#include "pycore_long.h" // _PyLong_GetZero()
#include "pycore_optimizer.h" // _PyUOpExecutor_Type, _PyUOpOptimizer_Type, ...
#include "pycore_pyerrors.h" // _PyErr_Occurred()
#include "pycore_pymem.h" // _PyMem_IsPtrFreed()
Expand Down Expand Up @@ -2970,3 +2971,52 @@ _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt)
{
Py_SET_REFCNT(ob, refcnt);
}


static PyObject* constants[] = {
&_Py_NoneStruct, // Py_NONE_IDX
(PyObject*)(&_Py_FalseStruct), // Py_FALSE_IDX
(PyObject*)(&_Py_TrueStruct), // Py_TRUE_IDX
&_Py_EllipsisObject, // Py_ELLIPSIS_IDX
&_Py_NotImplementedStruct, // Py_NOT_IMPLEMENTED_IDX
NULL, // Py_ZERO_IDX
NULL, // Py_ONE_IDX
NULL, // Py_EMPTY_STR_IDX
NULL, // Py_EMPTY_BYTES_IDX
NULL, // Py_EMPTY_TUPLE_IDX
};

void
_Py_GetConstant_Init(void)
{
constants[Py_ZERO_IDX] = _PyLong_GetZero();
constants[Py_ONE_IDX] = _PyLong_GetOne();
constants[Py_EMPTY_STR_IDX] = PyUnicode_New(0, 0);
constants[Py_EMPTY_BYTES_IDX] = PyBytes_FromStringAndSize(NULL, 0);
constants[Py_EMPTY_TUPLE_IDX] = PyTuple_New(0);
#ifndef NDEBUG
for (size_t i=0; i < Py_ARRAY_LENGTH(constants); i++) {
assert(constants[i] != NULL);
assert(_Py_IsImmortal(constants[i]));
}
#endif
}

PyObject*
Py_GetConstant(unsigned int constant_id)
{
if (constant_id <= Py_ARRAY_LENGTH(constants)) {
return constants[constant_id];
}
else {
return NULL;
}
}


PyObject*
Py_GetConstantBorrowed(unsigned int constant_id)
{
// All constants are immortal
return Py_GetConstant(constant_id);
}
Loading

0 comments on commit 2481445

Please sign in to comment.