Skip to content

Commit

Permalink
pythongh-115754: Add Py_GetConstantRef() function
Browse files Browse the repository at this point in the history
Add Py_GetConstantRef() and Py_GetConstant() 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_GetConstantRef() and Py_GetConstant() functions.
  • Loading branch information
vstinner committed Mar 15, 2024
1 parent 0c7dc49 commit 9a296f8
Show file tree
Hide file tree
Showing 19 changed files with 267 additions and 6 deletions.
25 changes: 25 additions & 0 deletions Doc/c-api/object.rst
Expand Up @@ -6,6 +6,31 @@ Object Protocol
===============


.. c:function:: PyObject* Py_GetConstantRef(unsigned int constant_id)
Get a :term:`strong reference` to a constant. Constant identifiers:
.. c:macro: Py_NONE
.. c:macro: Py_FALSE
.. c:macro: Py_TRUE
.. c:macro: Py_ELLIPSIS
.. c:macro: Py_NOT_IMPLEMENTED
.. c:macro: Py_ZERO
.. c:macro: Py_ONE
.. c:macro: Py_EMPTY_STR
.. c:macro: Py_EMPTY_BYTES
.. c:macro: Py_EMPTY_TUPLE
Return ``NULL`` if *constant_id* is invalid.
.. versionadded:: 3.13
.. c:function:: PyObject* Py_GetConstant(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
Expand Up @@ -1674,6 +1674,17 @@ New Features
more information.
(Contributed by Victor Stinner in :gh:`111696`.)

* Add :c:func:`Py_GetConstantRef` and :c:func:`Py_GetConstant` functions to
get constants. For example, ``Py_GetConstantRef(Py_ZERO)`` 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
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_GetConstant(Py_FALSE)
# define Py_True Py_GetConstant(Py_TRUE)
#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
31 changes: 29 additions & 2 deletions Include/object.h
Expand Up @@ -1069,12 +1069,34 @@ static inline PyObject* _Py_XNewRef(PyObject *obj)
#endif


#define Py_NONE 0
#define Py_FALSE 1
#define Py_TRUE 2
#define Py_ELLIPSIS 3
#define Py_NOT_IMPLEMENTED 4
#define Py_ZERO 5
#define Py_ONE 6
#define Py_EMPTY_STR 7
#define Py_EMPTY_BYTES 8
#define Py_EMPTY_TUPLE 9

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030d0000
PyAPI_FUNC(PyObject*) Py_GetConstantRef(unsigned int constant_id);
PyAPI_FUNC(PyObject*) Py_GetConstant(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_GetConstant(Py_NONE)
#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 @@ -1088,7 +1110,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_GetNotImplemented()
#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
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_GetEllipsis()
#else
# define Py_Ellipsis (&_Py_EllipsisObject)
#endif

/* Slice object interface */

Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_capi/test_object.py
@@ -0,0 +1,55 @@
import enum
import unittest
from test.support import import_helper
from enum import auto

_testlimitedcapi = import_helper.import_module('_testlimitedcapi')


class Constant(enum.IntEnum):
Py_NONE = 0
Py_FALSE = 1
Py_TRUE = 2
Py_ELLIPSIS = 3
Py_NOT_IMPLEMENTED = 4
Py_ZERO = 5
Py_ONE = 6
Py_EMPTY_STR = 7
Py_EMPTY_BYTES = 8
Py_EMPTY_TUPLE = 9

INVALID = Py_EMPTY_TUPLE + 1


class CAPITest(unittest.TestCase):
def check_get_constant(self, get_constant):
self.assertIs(get_constant(Constant.Py_NONE), None)
self.assertIs(get_constant(Constant.Py_FALSE), False)
self.assertIs(get_constant(Constant.Py_TRUE), True)
self.assertIs(get_constant(Constant.Py_ELLIPSIS), Ellipsis)
self.assertIs(get_constant(Constant.Py_NOT_IMPLEMENTED), NotImplemented)

for constant_id, constant_type, value in (
(Constant.Py_ZERO, int, 0),
(Constant.Py_ONE, int, 1),
(Constant.Py_EMPTY_STR, str, ""),
(Constant.Py_EMPTY_BYTES, bytes, b""),
(Constant.Py_EMPTY_TUPLE, 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)

def test_get_constant_ref(self):
self.check_get_constant(_testlimitedcapi.get_constant_ref)

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


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.

@@ -0,0 +1,3 @@
Add :c:func:`Py_GetConstantRef` and :c:func:`Py_GetConstant` functions to
get constants. For example, ``Py_GetConstantRef(Py_ZERO)`` returns a
:term:`strong reference` to the constant zero. Patch by Victor Stinner.
@@ -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
Expand Up @@ -2500,3 +2500,7 @@
added = '3.13'
[function.PyType_GetModuleName]
added = '3.13'
[function.Py_GetConstantRef]
added = '3.13'
[function.Py_GetConstant]
added = '3.13'
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
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/pyos.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/object.c _testlimitedcapi/pyos.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
Expand Up @@ -35,6 +35,9 @@ PyInit__testlimitedcapi(void)
if (_PyTestCapi_Init_HeaptypeRelative(mod) < 0) {
return NULL;
}
if (_PyTestCapi_Init_Object(mod) < 0) {
return NULL;
}
if (_PyTestCapi_Init_PyOS(mod) < 0) {
return NULL;
}
Expand Down
61 changes: 61 additions & 0 deletions Modules/_testlimitedcapi/object.c
@@ -0,0 +1,61 @@
// 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 Py_NewRef(obj);
}


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

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


static PyMethodDef test_methods[] = {
{"get_constant", get_constant, METH_VARARGS},
{"get_constant_ref", get_constant_ref, METH_VARARGS},
{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
Expand Up @@ -25,6 +25,7 @@
int _PyTestCapi_Init_ByteArray(PyObject *module);
int _PyTestCapi_Init_Bytes(PyObject *module);
int _PyTestCapi_Init_HeaptypeRelative(PyObject *module);
int _PyTestCapi_Init_Object(PyObject *module);
int _PyTestCapi_Init_PyOS(PyObject *module);
int _PyTestCapi_Init_Sys(PyObject *module);
int _PyTestCapi_Init_VectorcallLimited(PyObject *module);
Expand Down
49 changes: 49 additions & 0 deletions Objects/object.c
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,51 @@ _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt)
{
Py_SET_REFCNT(ob, refcnt);
}


static PyObject* constants[] = {
&_Py_NoneStruct, // Py_NONE id
(PyObject*)(&_Py_FalseStruct), // Py_FALSE id
(PyObject*)(&_Py_TrueStruct), // Py_TRUE id
&_Py_EllipsisObject, // Py_ELLIPSIS id
&_Py_NotImplementedStruct, // Py_NOT_IMPLEMENTED id
};

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

PyObject *obj;
switch (constant_id) {
case Py_ZERO:
obj = _PyLong_GetZero();
break;
case Py_ONE:
obj = _PyLong_GetOne();
break;
case Py_EMPTY_STR:
obj = PyUnicode_New(0, 0);
break;
case Py_EMPTY_BYTES:
obj = PyBytes_FromStringAndSize(NULL, 0);
break;
case Py_EMPTY_TUPLE:
obj = PyTuple_New(0);
break;
default:
return NULL;
}
return obj;
}


PyObject*
Py_GetConstantRef(unsigned int constant_id)
{
PyObject *obj = Py_GetConstant(constant_id);
assert(obj == NULL || _Py_IsImmortal(obj));
return obj;
}
2 changes: 2 additions & 0 deletions PC/python3dll.c

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

1 change: 1 addition & 0 deletions PCbuild/_testlimitedcapi.vcxproj
Expand Up @@ -97,6 +97,7 @@
<ClCompile Include="..\Modules\_testlimitedcapi\bytearray.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\bytes.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\heaptype_relative.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\object.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\pyos.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\sys.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\vectorcall_limited.c" />
Expand Down
1 change: 1 addition & 0 deletions PCbuild/_testlimitedcapi.vcxproj.filters
Expand Up @@ -12,6 +12,7 @@
<ClCompile Include="..\Modules\_testlimitedcapi\bytearray.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\bytes.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\heaptype_relative.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\object.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\pyos.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\sys.c" />
<ClCompile Include="..\Modules\_testlimitedcapi\vectorcall_limited.c" />
Expand Down

0 comments on commit 9a296f8

Please sign in to comment.