gh-111506: Add _Py_OPAQUE_PYOBJECT to hide PyObject layout & related API (GH-136505)

Allow Py_LIMITED_API for (Py_GIL_DISABLED && _Py_OPAQUE_PYOBJECT)


API that's removed when _Py_OPAQUE_PYOBJECT is defined:

    - PyObject_HEAD
    - _PyObject_EXTRA_INIT
    - PyObject_HEAD_INIT
    - PyObject_VAR_HEAD
    - struct _object (i.e. PyObject) (opaque)
    - struct PyVarObject (opaque)
    - Py_SIZE
    - Py_SET_TYPE
    - Py_SET_SIZE
    - PyModuleDef_Base (opaque)
    - PyModuleDef_HEAD_INIT
    - PyModuleDef (opaque)
    - _Py_IsImmortal
    - _Py_IsStaticImmortal

Note that the `_Py_IsImmortal` removal (and a few other issues)
 means _Py_OPAQUE_PYOBJECT only works with limited
API 3.14+ now.


Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Petr Viktorin 2025-07-12 09:55:12 +02:00 committed by GitHub
parent db47f4d844
commit c7d24b81c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 123 additions and 25 deletions

View file

@ -45,19 +45,19 @@
# endif # endif
#endif #endif
// gh-111506: The free-threaded build is not compatible with the limited API #if defined(Py_GIL_DISABLED)
// or the stable ABI. # if defined(Py_LIMITED_API) && !defined(_Py_OPAQUE_PYOBJECT)
#if defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED) # error "Py_LIMITED_API is not currently supported in the free-threaded build"
# error "The limited API is not currently supported in the free-threaded build"
# endif # endif
#if defined(Py_GIL_DISABLED) && defined(_MSC_VER) # if defined(_MSC_VER)
# include <intrin.h> // __readgsqword() # include <intrin.h> // __readgsqword()
# endif # endif
#if defined(Py_GIL_DISABLED) && defined(__MINGW32__) # if defined(__MINGW32__)
# include <intrin.h> // __readgsqword() # include <intrin.h> // __readgsqword()
# endif # endif
#endif // Py_GIL_DISABLED
// Include Python header files // Include Python header files
#include "pyport.h" #include "pyport.h"

View file

@ -36,6 +36,7 @@ PyAPI_FUNC(PyObject *) PyModuleDef_Init(PyModuleDef*);
PyAPI_DATA(PyTypeObject) PyModuleDef_Type; PyAPI_DATA(PyTypeObject) PyModuleDef_Type;
#endif #endif
#ifndef _Py_OPAQUE_PYOBJECT
typedef struct PyModuleDef_Base { typedef struct PyModuleDef_Base {
PyObject_HEAD PyObject_HEAD
/* The function used to re-initialize the module. /* The function used to re-initialize the module.
@ -63,6 +64,7 @@ typedef struct PyModuleDef_Base {
0, /* m_index */ \ 0, /* m_index */ \
_Py_NULL, /* m_copy */ \ _Py_NULL, /* m_copy */ \
} }
#endif // _Py_OPAQUE_PYOBJECT
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000
/* New in 3.5 */ /* New in 3.5 */
@ -104,6 +106,8 @@ struct PyModuleDef_Slot {
PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil); PyAPI_FUNC(int) PyUnstable_Module_SetGIL(PyObject *module, void *gil);
#endif #endif
#ifndef _Py_OPAQUE_PYOBJECT
struct PyModuleDef { struct PyModuleDef {
PyModuleDef_Base m_base; PyModuleDef_Base m_base;
const char* m_name; const char* m_name;
@ -115,6 +119,7 @@ struct PyModuleDef {
inquiry m_clear; inquiry m_clear;
freefunc m_free; freefunc m_free;
}; };
#endif // _Py_OPAQUE_PYOBJECT
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -56,6 +56,11 @@ whose size is determined when the object is allocated.
# define Py_REF_DEBUG # define Py_REF_DEBUG
#endif #endif
#if defined(_Py_OPAQUE_PYOBJECT) && !defined(Py_LIMITED_API)
# error "_Py_OPAQUE_PYOBJECT only makes sense with Py_LIMITED_API"
#endif
#ifndef _Py_OPAQUE_PYOBJECT
/* PyObject_HEAD defines the initial segment of every PyObject. */ /* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD PyObject ob_base; #define PyObject_HEAD PyObject ob_base;
@ -99,6 +104,8 @@ whose size is determined when the object is allocated.
* not necessarily a byte count. * not necessarily a byte count.
*/ */
#define PyObject_VAR_HEAD PyVarObject ob_base; #define PyObject_VAR_HEAD PyVarObject ob_base;
#endif // !defined(_Py_OPAQUE_PYOBJECT)
#define Py_INVALID_SIZE (Py_ssize_t)-1 #define Py_INVALID_SIZE (Py_ssize_t)-1
/* PyObjects are given a minimum alignment so that the least significant bits /* PyObjects are given a minimum alignment so that the least significant bits
@ -112,7 +119,9 @@ whose size is determined when the object is allocated.
* by hand. Similarly every pointer to a variable-size Python object can, * by hand. Similarly every pointer to a variable-size Python object can,
* in addition, be cast to PyVarObject*. * in addition, be cast to PyVarObject*.
*/ */
#ifndef Py_GIL_DISABLED #ifdef _Py_OPAQUE_PYOBJECT
/* PyObject is opaque */
#elif !defined(Py_GIL_DISABLED)
struct _object { struct _object {
#if (defined(__GNUC__) || defined(__clang__)) \ #if (defined(__GNUC__) || defined(__clang__)) \
&& !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L) && !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L)
@ -168,15 +177,18 @@ struct _object {
Py_ssize_t ob_ref_shared; // shared (atomic) reference count Py_ssize_t ob_ref_shared; // shared (atomic) reference count
PyTypeObject *ob_type; PyTypeObject *ob_type;
}; };
#endif #endif // !defined(_Py_OPAQUE_PYOBJECT)
/* Cast argument to PyObject* type. */ /* Cast argument to PyObject* type. */
#define _PyObject_CAST(op) _Py_CAST(PyObject*, (op)) #define _PyObject_CAST(op) _Py_CAST(PyObject*, (op))
typedef struct { #ifndef _Py_OPAQUE_PYOBJECT
struct PyVarObject {
PyObject ob_base; PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */ Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject; };
#endif
typedef struct PyVarObject PyVarObject;
/* Cast argument to PyVarObject* type. */ /* Cast argument to PyVarObject* type. */
#define _PyVarObject_CAST(op) _Py_CAST(PyVarObject*, (op)) #define _PyVarObject_CAST(op) _Py_CAST(PyVarObject*, (op))
@ -286,6 +298,7 @@ PyAPI_FUNC(PyTypeObject*) Py_TYPE(PyObject *ob);
PyAPI_DATA(PyTypeObject) PyLong_Type; PyAPI_DATA(PyTypeObject) PyLong_Type;
PyAPI_DATA(PyTypeObject) PyBool_Type; PyAPI_DATA(PyTypeObject) PyBool_Type;
#ifndef _Py_OPAQUE_PYOBJECT
// bpo-39573: The Py_SET_SIZE() function must be used to set an object size. // bpo-39573: The Py_SET_SIZE() function must be used to set an object size.
static inline Py_ssize_t Py_SIZE(PyObject *ob) { static inline Py_ssize_t Py_SIZE(PyObject *ob) {
assert(Py_TYPE(ob) != &PyLong_Type); assert(Py_TYPE(ob) != &PyLong_Type);
@ -295,6 +308,7 @@ static inline Py_ssize_t Py_SIZE(PyObject *ob) {
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000
# define Py_SIZE(ob) Py_SIZE(_PyObject_CAST(ob)) # define Py_SIZE(ob) Py_SIZE(_PyObject_CAST(ob))
#endif #endif
#endif // !defined(_Py_OPAQUE_PYOBJECT)
static inline int Py_IS_TYPE(PyObject *ob, PyTypeObject *type) { static inline int Py_IS_TYPE(PyObject *ob, PyTypeObject *type) {
return Py_TYPE(ob) == type; return Py_TYPE(ob) == type;
@ -304,6 +318,7 @@ static inline int Py_IS_TYPE(PyObject *ob, PyTypeObject *type) {
#endif #endif
#ifndef _Py_OPAQUE_PYOBJECT
static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) { static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) {
ob->ob_type = type; ob->ob_type = type;
} }
@ -323,6 +338,7 @@ static inline void Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) {
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000
# define Py_SET_SIZE(ob, size) Py_SET_SIZE(_PyVarObject_CAST(ob), (size)) # define Py_SET_SIZE(ob, size) Py_SET_SIZE(_PyVarObject_CAST(ob), (size))
#endif #endif
#endif // !defined(_Py_OPAQUE_PYOBJECT)
/* /*

View file

@ -117,6 +117,7 @@ PyAPI_FUNC(Py_ssize_t) Py_REFCNT(PyObject *ob);
#endif #endif
#endif #endif
#ifndef _Py_OPAQUE_PYOBJECT
static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op) static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op)
{ {
#if defined(Py_GIL_DISABLED) #if defined(Py_GIL_DISABLED)
@ -140,6 +141,7 @@ static inline Py_ALWAYS_INLINE int _Py_IsStaticImmortal(PyObject *op)
#endif #endif
} }
#define _Py_IsStaticImmortal(op) _Py_IsStaticImmortal(_PyObject_CAST(op)) #define _Py_IsStaticImmortal(op) _Py_IsStaticImmortal(_PyObject_CAST(op))
#endif // !defined(_Py_OPAQUE_PYOBJECT)
// Py_SET_REFCNT() implementation for stable ABI // Py_SET_REFCNT() implementation for stable ABI
PyAPI_FUNC(void) _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt); PyAPI_FUNC(void) _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt);

View file

@ -12,7 +12,10 @@
from test import support from test import support
SOURCE = os.path.join(os.path.dirname(__file__), 'extension.c') SOURCES = [
os.path.join(os.path.dirname(__file__), 'extension.c'),
os.path.join(os.path.dirname(__file__), 'create_moduledef.c'),
]
SETUP = os.path.join(os.path.dirname(__file__), 'setup.py') SETUP = os.path.join(os.path.dirname(__file__), 'setup.py')
@ -35,17 +38,22 @@ class BaseTests:
def test_build(self): def test_build(self):
self.check_build('_test_cext') self.check_build('_test_cext')
def check_build(self, extension_name, std=None, limited=False): def check_build(self, extension_name, std=None, limited=False,
opaque_pyobject=False):
venv_dir = 'env' venv_dir = 'env'
with support.setup_venv_with_pip_setuptools(venv_dir) as python_exe: with support.setup_venv_with_pip_setuptools(venv_dir) as python_exe:
self._check_build(extension_name, python_exe, self._check_build(extension_name, python_exe,
std=std, limited=limited) std=std, limited=limited,
opaque_pyobject=opaque_pyobject)
def _check_build(self, extension_name, python_exe, std, limited): def _check_build(self, extension_name, python_exe, std, limited,
opaque_pyobject):
pkg_dir = 'pkg' pkg_dir = 'pkg'
os.mkdir(pkg_dir) os.mkdir(pkg_dir)
shutil.copy(SETUP, os.path.join(pkg_dir, os.path.basename(SETUP))) shutil.copy(SETUP, os.path.join(pkg_dir, os.path.basename(SETUP)))
shutil.copy(SOURCE, os.path.join(pkg_dir, os.path.basename(SOURCE))) for source in SOURCES:
dest = os.path.join(pkg_dir, os.path.basename(source))
shutil.copy(source, dest)
def run_cmd(operation, cmd): def run_cmd(operation, cmd):
env = os.environ.copy() env = os.environ.copy()
@ -53,6 +61,8 @@ def run_cmd(operation, cmd):
env['CPYTHON_TEST_STD'] = std env['CPYTHON_TEST_STD'] = std
if limited: if limited:
env['CPYTHON_TEST_LIMITED'] = '1' env['CPYTHON_TEST_LIMITED'] = '1'
if opaque_pyobject:
env['CPYTHON_TEST_OPAQUE_PYOBJECT'] = '1'
env['CPYTHON_TEST_EXT_NAME'] = extension_name env['CPYTHON_TEST_EXT_NAME'] = extension_name
env['TEST_INTERNAL_C_API'] = str(int(self.TEST_INTERNAL_C_API)) env['TEST_INTERNAL_C_API'] = str(int(self.TEST_INTERNAL_C_API))
if support.verbose: if support.verbose:
@ -107,6 +117,11 @@ def test_build_limited_c11(self):
def test_build_c11(self): def test_build_c11(self):
self.check_build('_test_c11_cext', std='c11') self.check_build('_test_c11_cext', std='c11')
def test_build_opaque_pyobject(self):
# Test with _Py_OPAQUE_PYOBJECT
self.check_build('_test_limited_opaque_cext', limited=True,
opaque_pyobject=True)
@unittest.skipIf(support.MS_WINDOWS, "MSVC doesn't support /std:c99") @unittest.skipIf(support.MS_WINDOWS, "MSVC doesn't support /std:c99")
def test_build_c99(self): def test_build_c99(self):
# In public docs, we say C API is compatible with C11. However, # In public docs, we say C API is compatible with C11. However,

View file

@ -0,0 +1,29 @@
// Workaround for testing _Py_OPAQUE_PYOBJECT.
// See end of 'extension.c'
#undef _Py_OPAQUE_PYOBJECT
#undef Py_LIMITED_API
#include "Python.h"
// (repeated definition to avoid creating a header)
extern PyObject *testcext_create_moduledef(
const char *name, const char *doc,
PyMethodDef *methods, PyModuleDef_Slot *slots);
PyObject *testcext_create_moduledef(
const char *name, const char *doc,
PyMethodDef *methods, PyModuleDef_Slot *slots) {
static struct PyModuleDef _testcext_module = {
PyModuleDef_HEAD_INIT,
};
if (!_testcext_module.m_name) {
_testcext_module.m_name = name;
_testcext_module.m_doc = doc;
_testcext_module.m_methods = methods;
_testcext_module.m_slots = slots;
}
return PyModuleDef_Init(&_testcext_module);
}

View file

@ -78,6 +78,9 @@ _testcext_exec(
return 0; return 0;
} }
#define _FUNC_NAME(NAME) PyInit_ ## NAME
#define FUNC_NAME(NAME) _FUNC_NAME(NAME)
// Converting from function pointer to void* has undefined behavior, but // Converting from function pointer to void* has undefined behavior, but
// works on all known platforms, and CPython's module and type slots currently // works on all known platforms, and CPython's module and type slots currently
// need it. // need it.
@ -96,9 +99,10 @@ static PyModuleDef_Slot _testcext_slots[] = {
_Py_COMP_DIAG_POP _Py_COMP_DIAG_POP
PyDoc_STRVAR(_testcext_doc, "C test extension."); PyDoc_STRVAR(_testcext_doc, "C test extension.");
#ifndef _Py_OPAQUE_PYOBJECT
static struct PyModuleDef _testcext_module = { static struct PyModuleDef _testcext_module = {
PyModuleDef_HEAD_INIT, // m_base PyModuleDef_HEAD_INIT, // m_base
STR(MODULE_NAME), // m_name STR(MODULE_NAME), // m_name
@ -112,11 +116,30 @@ static struct PyModuleDef _testcext_module = {
}; };
#define _FUNC_NAME(NAME) PyInit_ ## NAME
#define FUNC_NAME(NAME) _FUNC_NAME(NAME)
PyMODINIT_FUNC PyMODINIT_FUNC
FUNC_NAME(MODULE_NAME)(void) FUNC_NAME(MODULE_NAME)(void)
{ {
return PyModuleDef_Init(&_testcext_module); return PyModuleDef_Init(&_testcext_module);
} }
#else // _Py_OPAQUE_PYOBJECT
// Opaque PyObject means that PyModuleDef is also opaque and cannot be
// declared statically. See PEP 793.
// So, this part of module creation is split into a separate source file
// which uses non-limited API.
// (repeated definition to avoid creating a header)
extern PyObject *testcext_create_moduledef(
const char *name, const char *doc,
PyMethodDef *methods, PyModuleDef_Slot *slots);
PyMODINIT_FUNC
FUNC_NAME(MODULE_NAME)(void)
{
return testcext_create_moduledef(
STR(MODULE_NAME), _testcext_doc, _testcext_methods, _testcext_slots);
}
#endif // _Py_OPAQUE_PYOBJECT

View file

@ -59,8 +59,11 @@ def main():
std = os.environ.get("CPYTHON_TEST_STD", "") std = os.environ.get("CPYTHON_TEST_STD", "")
module_name = os.environ["CPYTHON_TEST_EXT_NAME"] module_name = os.environ["CPYTHON_TEST_EXT_NAME"]
limited = bool(os.environ.get("CPYTHON_TEST_LIMITED", "")) limited = bool(os.environ.get("CPYTHON_TEST_LIMITED", ""))
opaque_pyobject = bool(os.environ.get("CPYTHON_TEST_OPAQUE_PYOBJECT", ""))
internal = bool(int(os.environ.get("TEST_INTERNAL_C_API", "0"))) internal = bool(int(os.environ.get("TEST_INTERNAL_C_API", "0")))
sources = [SOURCE]
if not internal: if not internal:
cflags = list(PUBLIC_CFLAGS) cflags = list(PUBLIC_CFLAGS)
else: else:
@ -93,6 +96,11 @@ def main():
version = sys.hexversion version = sys.hexversion
cflags.append(f'-DPy_LIMITED_API={version:#x}') cflags.append(f'-DPy_LIMITED_API={version:#x}')
# Define _Py_OPAQUE_PYOBJECT macro
if opaque_pyobject:
cflags.append(f'-D_Py_OPAQUE_PYOBJECT')
sources.append('create_moduledef.c')
if internal: if internal:
cflags.append('-DTEST_INTERNAL_C_API=1') cflags.append('-DTEST_INTERNAL_C_API=1')
@ -120,7 +128,7 @@ def main():
ext = Extension( ext = Extension(
module_name, module_name,
sources=[SOURCE], sources=sources,
extra_compile_args=cflags, extra_compile_args=cflags,
include_dirs=include_dirs, include_dirs=include_dirs,
library_dirs=library_dirs) library_dirs=library_dirs)