From 9a296f8f107b58d20bdf034fd93e75311fe5de9f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 15 Mar 2024 23:14:33 +0100 Subject: [PATCH] gh-115754: Add Py_GetConstantRef() function 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. --- Doc/c-api/object.rst | 25 ++++++++ Doc/data/stable_abi.dat | 2 + Doc/whatsnew/3.13.rst | 11 ++++ Include/boolobject.h | 9 ++- Include/object.h | 31 +++++++++- Include/sliceobject.h | 6 +- Lib/test/test_capi/test_object.py | 55 +++++++++++++++++ Lib/test/test_stable_abi_ctypes.py | 2 + ...-03-15-23-55-24.gh-issue-115754.xnzc__.rst | 3 + ...-03-15-23-57-33.gh-issue-115754.zLdv82.rst | 5 ++ Misc/stable_abi.toml | 4 ++ Modules/Setup.stdlib.in | 2 +- Modules/_testlimitedcapi.c | 3 + Modules/_testlimitedcapi/object.c | 61 +++++++++++++++++++ Modules/_testlimitedcapi/parts.h | 1 + Objects/object.c | 49 +++++++++++++++ PC/python3dll.c | 2 + PCbuild/_testlimitedcapi.vcxproj | 1 + PCbuild/_testlimitedcapi.vcxproj.filters | 1 + 19 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 Lib/test/test_capi/test_object.py create mode 100644 Misc/NEWS.d/next/C API/2024-03-15-23-55-24.gh-issue-115754.xnzc__.rst create mode 100644 Misc/NEWS.d/next/C API/2024-03-15-23-57-33.gh-issue-115754.zLdv82.rst create mode 100644 Modules/_testlimitedcapi/object.c diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 12476412799a4f1..d9b74d79e3dd3d8 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -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 diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 9d0ad3d036dac34..ae4048332e3157f 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -838,6 +838,8 @@ function,Py_GenericAlias,3.9,, var,Py_GenericAliasType,3.9,, function,Py_GetBuildInfo,3.2,, function,Py_GetCompiler,3.2,, +function,Py_GetConstant,3.13,, +function,Py_GetConstantRef,3.13,, function,Py_GetCopyright,3.2,, function,Py_GetExecPrefix,3.2,, function,Py_GetPath,3.2,, diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 03ae60189053800..86b2847295be22e 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -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 ---------------------- diff --git a/Include/boolobject.h b/Include/boolobject.h index 19aef5b1b87c6ae..45868c3442f9f94 100644 --- a/Include/boolobject.h +++ b/Include/boolobject.h @@ -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); diff --git a/Include/object.h b/Include/object.h index 34141af7b7f7ef7..f867996b99ef377 100644 --- a/Include/object.h +++ b/Include/object.h @@ -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); @@ -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 diff --git a/Include/sliceobject.h b/Include/sliceobject.h index c13863f27c2e638..75c591277a612d5 100644 --- a/Include/sliceobject.h +++ b/Include/sliceobject.h @@ -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 */ diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py new file mode 100644 index 000000000000000..f3be71b50e9dbb8 --- /dev/null +++ b/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() diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index 117c27d27b38dcb..12c9427c97a0faf 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -856,6 +856,8 @@ def test_windows_feature_macros(self): "Py_GetArgcArgv", "Py_GetBuildInfo", "Py_GetCompiler", + "Py_GetConstant", + "Py_GetConstantRef", "Py_GetCopyright", "Py_GetExecPrefix", "Py_GetPath", diff --git a/Misc/NEWS.d/next/C API/2024-03-15-23-55-24.gh-issue-115754.xnzc__.rst b/Misc/NEWS.d/next/C API/2024-03-15-23-55-24.gh-issue-115754.xnzc__.rst new file mode 100644 index 000000000000000..a9450e9b2e789c3 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2024-03-15-23-55-24.gh-issue-115754.xnzc__.rst @@ -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. diff --git a/Misc/NEWS.d/next/C API/2024-03-15-23-57-33.gh-issue-115754.zLdv82.rst b/Misc/NEWS.d/next/C API/2024-03-15-23-57-33.gh-issue-115754.zLdv82.rst new file mode 100644 index 000000000000000..feff0c0897eae18 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2024-03-15-23-57-33.gh-issue-115754.zLdv82.rst @@ -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. diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index c68adf8db079f99..c260bcf2928f396 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -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' diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index deada66cf1a8072..74ca3059e534627 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -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 diff --git a/Modules/_testlimitedcapi.c b/Modules/_testlimitedcapi.c index 49bf6a3ea39613f..b35c0ed94bfdd7c 100644 --- a/Modules/_testlimitedcapi.c +++ b/Modules/_testlimitedcapi.c @@ -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; } diff --git a/Modules/_testlimitedcapi/object.c b/Modules/_testlimitedcapi/object.c new file mode 100644 index 000000000000000..cea187b7e722f6e --- /dev/null +++ b/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; +} diff --git a/Modules/_testlimitedcapi/parts.h b/Modules/_testlimitedcapi/parts.h index 9bc52413382eb57..f1af974e89c1171 100644 --- a/Modules/_testlimitedcapi/parts.h +++ b/Modules/_testlimitedcapi/parts.h @@ -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); diff --git a/Objects/object.c b/Objects/object.c index df14fe0c6fbfec1..cb40c88ec7d8dd8 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -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() @@ -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; +} diff --git a/PC/python3dll.c b/PC/python3dll.c index dbfa3f23bb586df..ad4bb317f5d9977 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -55,6 +55,8 @@ EXPORT_FUNC(Py_GenericAlias) EXPORT_FUNC(Py_GetArgcArgv) EXPORT_FUNC(Py_GetBuildInfo) EXPORT_FUNC(Py_GetCompiler) +EXPORT_FUNC(Py_GetConstant) +EXPORT_FUNC(Py_GetConstantRef) EXPORT_FUNC(Py_GetCopyright) EXPORT_FUNC(Py_GetExecPrefix) EXPORT_FUNC(Py_GetPath) diff --git a/PCbuild/_testlimitedcapi.vcxproj b/PCbuild/_testlimitedcapi.vcxproj index 1afeacaa93396e0..f62eb4c9ca24827 100644 --- a/PCbuild/_testlimitedcapi.vcxproj +++ b/PCbuild/_testlimitedcapi.vcxproj @@ -97,6 +97,7 @@ + diff --git a/PCbuild/_testlimitedcapi.vcxproj.filters b/PCbuild/_testlimitedcapi.vcxproj.filters index b3eeb86185c3729..753e5f767a0d1d9 100644 --- a/PCbuild/_testlimitedcapi.vcxproj.filters +++ b/PCbuild/_testlimitedcapi.vcxproj.filters @@ -12,6 +12,7 @@ +