* Prototype of C extension using Python limited API
@ 2026-05-28 16:25 Matthieu Longo via Gdb
2026-05-28 16:30 ` Matthieu Longo via Gdb
0 siblings, 1 reply; 2+ messages in thread
From: Matthieu Longo via Gdb @ 2026-05-28 16:25 UTC (permalink / raw)
To: Tom Tromey, gdb; +Cc: Andrew Burgess
[-- Attachment #1: Type: text/plain, Size: 940 bytes --]
Hi Tom,
As I previously told you (at least, I believe so but I am not 100% sure), I finished migrating all
the Python C extensions some time ago. I had discovered some issues with a case of inheritance
causing unexpected crashes.
To facilitate the debugging, I decided to implement a prototype outside of GDB, and managed to make
it work. I finally got the prototype reviewed by Victor Stinner, a Python core developer, to
double-check whether my design was working as expected.
Please find attached my last iteration. I also provided an archive which should be more convenient
for you if you want to compile and test it.
Could you please review it so that we validate this new approach before I migrate all my previous
patches to it ?
PS:
- a README is provided with all the needed instruction inside.
- this patch contains code from outside GDB's repository, and should not be merged inside GDB's
repository.
Regards,
Matthieu
[-- Attachment #2: prototype.patch --]
[-- Type: text/plain, Size: 65251 bytes --]
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..d320000
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,55 @@
+cmake_minimum_required(VERSION 4.3)
+project(PythonCppExtension)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+find_package(Python3 "3.14" COMPONENTS Interpreter Development)
+
+set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdiagnostics-color=always")
+set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always")
+
+#set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fsanitize=leak")
+#set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=leak")
+
+set(SRCS
+ "src/gdb-others.cpp"
+ "src/py-obj-type.cpp"
+ "src/python-internal.cpp"
+ "src/my_py_module.cpp"
+ "src/A.cpp"
+ "src/B.cpp"
+ "src/ObjWithAlloc.cpp")
+add_library(MyModule SHARED ${SRCS})
+set_target_properties(
+ MyModule
+ PROPERTIES
+ PREFIX ""
+ OUTPUT_NAME "MyModule"
+ LINKER_LANGUAGE C
+ LINK_LIBRARIES Python3::Python
+ INCLUDE_DIRECTORIES "${PYTHON_INCLUDE_DIR};${CMAKE_CURRENT_SOURCE_DIR}/include;${CMAKE_CURRENT_SOURCE_DIR}/src"
+ COMPILE_DEFINITIONS "Py_LIMITED_API=0x030e0000"
+ )
+
+add_executable(PyDebug "pydbg.cpp")
+target_link_libraries(PyDebug ${PYTHON_LIBRARIES})
+set_target_properties(
+ PyDebug
+ PROPERTIES
+ PREFIX ""
+ OUTPUT_NAME "PyDebug"
+ LINKER_LANGUAGE C
+ LINK_LIBRARIES Python3::Python
+ INCLUDE_DIRECTORIES "${PYTHON_INCLUDE_DIR};${CMAKE_CURRENT_SOURCE_DIR}"
+ )
+
+add_custom_target(test-pydebug
+ COMMAND cmake -E env PYTHONPATH=${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/PyDebug 2>&1
+ VERBATIM)
+add_dependencies(test-pydebug PyDebug)
+
+add_custom_target(test-py
+ COMMAND cmake -E env PYTHONPATH=${CMAKE_BINARY_DIR} python3 ${CMAKE_CURRENT_SOURCE_DIR}/test.py
+ VERBATIM)
+add_dependencies(test-py MyModule)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cc9fafe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+## Build
+
+```bash
+cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -B build-dir
+```
+
+## Testing
+
+```bash
+cd build-dir
+ninja && ninja test-pydebug
+# OR to get the error message that is swalled by CMake
+PYTHONPATH=$(pwd) ./PyDebug
+```
+
+### Expected output
+
+```text
+['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', '_cycle', 'foo', 'iama', 'iamb', 'name']
+B Hello I am A Hello I am both A and B
+Calling E::tp_traverse_impl
+--> B_traverse
+E: calling MyModule.A::tp_traverse
+Calling E::tp_traverse_impl
+--> A_traverse
+visiting __dict__
+E: calling Py_VISIT (Py_TYPE (self))
+E: skipping Py_VISIT (Py_TYPE (self)), will call it in MyModule.A
+Calling E::tp_traverse_impl
+--> B_traverse
+E: calling MyModule.A::tp_traverse
+Calling E::tp_traverse_impl
+--> A_traverse
+visiting __dict__
+E: calling Py_VISIT (Py_TYPE (self))
+E: skipping Py_VISIT (Py_TYPE (self)), will call it in MyModule.A
+Calling (null)::tp_clear_impl
+--> B_clear
+(null): calling MyModule.A::tp_clear
+Unexcepted error occurred (TO INVESTIGATE)
+AttributeError: __module__
+Calling (null)::tp_clear_impl
+--> A_clear
+clearing __dict__
+Exception ignored in tp_clear of E:
+AttributeError: __module__
+Calling (null)::tp_dealloc
+Unexcepted error occurred (TO INVESTIGATE)
+AttributeError: __module__
+Calling (null)::tp_clear_impl
+--> B_clear
+(null): calling MyModule.A::tp_clear
+Unexcepted error occurred (TO INVESTIGATE)
+AttributeError: __module__
+Calling (null)::tp_clear_impl
+--> A_clear
+clearing __dict__
+Deleting type (null)
+```
+
+### Issues
+
+1. Not sure why A_clear and B_clear are called twice. Is it due to the cycle ?
+2. The type name is `(null)` because `PyType_GetFullyQualifiedName()` returns `NULL` for an unknown reason. Please could you explain me why ?
+3. There is an unexpected error `AttributeError: __module__`. I don't understand where it comes from.
diff --git a/include/gdb-others.hpp b/include/gdb-others.hpp
new file mode 100644
index 0000000..36798b3
--- /dev/null
+++ b/include/gdb-others.hpp
@@ -0,0 +1,64 @@
+#pragma once
+
+#include <Python.h>
+#include <cstddef>
+
+extern "C" {
+
+#define ATTRIBUTE_UNUSED_RESULT __attribute__ ((__warn_unused_result__))
+#define ATTRIBUTE_USED __attribute__ ((__used__))
+#define ATTRIBUTE_UNUSED __attribute__((unused))
+
+#define gdb_assert(expr) \
+ ((void) ((expr) ? 0 : \
+ (gdb_assert_fail (#expr, __FILE__, __LINE__, __func__), 0)))
+
+/* This prints an "Assertion failed" message, asking the user if they
+ want to continue, dump core, or just exit. */
+#define gdb_assert_fail(assertion, file, line, function) \
+ internal_error_loc (file, line, "%s: Assertion `%s' failed.", \
+ function, assertion)
+
+/* The canonical form of gdb_assert (0).
+ MESSAGE is a string to include in the error message. */
+
+#define gdb_assert_not_reached(message, ...) \
+ internal_error_loc (__FILE__, __LINE__, _("%s: " message), __func__, \
+ ##__VA_ARGS__)
+
+[[noreturn]] extern void internal_error_loc (const char *file, int line,
+ const char *fmt, ...)
+ __attribute__ ((format (printf, 3, 4)));;
+
+}
+
+
+/* Wrap PyGetSetDef to allow convenient construction with string
+ literals. Unfortunately, PyGetSetDef's 'name' and 'doc' members
+ are 'char *' instead of 'const char *', meaning that in order to
+ list-initialize PyGetSetDef arrays with string literals (and
+ without the wrapping below) would require writing explicit 'char *'
+ casts. Instead, we extend PyGetSetDef and add constexpr
+ constructors that accept const 'name' and 'doc', hiding the ugly
+ casts here in a single place. */
+
+struct gdb_PyGetSetDef : PyGetSetDef
+{
+ constexpr gdb_PyGetSetDef (const char *name_, getter get_, setter set_,
+ const char *doc_, void *closure_)
+ : PyGetSetDef {const_cast<char *> (name_), get_, set_,
+ const_cast<char *> (doc_), closure_}
+ {}
+
+ /* Alternative constructor that allows omitting the closure in list
+ initialization. */
+ constexpr gdb_PyGetSetDef (const char *name_, getter get_, setter set_,
+ const char *doc_)
+ : gdb_PyGetSetDef {name_, get_, set_, doc_, NULL}
+ {}
+
+ /* Constructor for the sentinel entries. */
+ constexpr gdb_PyGetSetDef (std::nullptr_t)
+ : gdb_PyGetSetDef {NULL, NULL, NULL, NULL, NULL}
+ {}
+};
diff --git a/include/gdb_ref_ptr.hpp b/include/gdb_ref_ptr.hpp
new file mode 100644
index 0000000..379850f
--- /dev/null
+++ b/include/gdb_ref_ptr.hpp
@@ -0,0 +1,301 @@
+/* Reference-counted smart pointer class
+
+ Copyright (C) 2016-2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDBSUPPORT_GDB_REF_PTR_H
+#define GDBSUPPORT_GDB_REF_PTR_H
+
+#include <cstddef>
+
+struct gdbpy_ref_policy;
+
+namespace gdb
+{
+
+/* An instance of this class either holds a reference to a
+ reference-counted object or is "NULL". Reference counting is
+ handled externally by a policy class. If the object holds a
+ reference, then when the object is destroyed, the reference is
+ decref'd.
+
+ Normally an instance is constructed using a pointer. This sort of
+ initialization lets this class manage the lifetime of that
+ reference.
+
+ Assignment and copy construction will make a new reference as
+ appropriate. Assignment from a plain pointer is disallowed to
+ avoid confusion about whether this acquires a new reference;
+ instead use the "reset" method -- which, like the pointer
+ constructor, transfers ownership.
+
+ The policy class must provide two static methods:
+ void incref (T *);
+ void decref (T *);
+*/
+template<typename T, typename Policy>
+class ref_ptr
+{
+public:
+
+ /* Befriend all instantiations of this template, so that the
+ templated copy constructors and assignment operators can access
+ the data. */
+ template<typename X, typename Y> friend class ref_ptr;
+
+ /* Create a new NULL instance. */
+ ref_ptr () noexcept
+ : m_obj (NULL)
+ {
+ }
+
+ /* Create a new NULL instance. Note that this is not explicit. */
+ ref_ptr (const std::nullptr_t) noexcept
+ : m_obj (NULL)
+ {
+ }
+
+ /* Create a new instance. OBJ is a reference, management of which
+ is now transferred to this class. */
+ explicit ref_ptr (T *obj) noexcept
+ : m_obj (obj)
+ {
+ }
+
+ /* Copy another instance. */
+ template<typename U,
+ typename = std::is_convertible<U *, T*>>
+ ref_ptr (const ref_ptr<U, Policy> &other)
+ : m_obj (other.m_obj)
+ {
+ if (m_obj != NULL)
+ Policy::incref (m_obj);
+ }
+
+ ref_ptr (const ref_ptr &other)
+ : m_obj (other.m_obj)
+ {
+ if (m_obj != NULL)
+ Policy::incref (m_obj);
+ }
+
+ /* Transfer ownership from OTHER. */
+ template<typename U,
+ typename = std::is_convertible<U *, T*>>
+ ref_ptr (ref_ptr<U, Policy> &&other) noexcept
+ : m_obj (other.m_obj)
+ {
+ other.m_obj = NULL;
+ }
+
+ ref_ptr (ref_ptr &&other) noexcept
+ : m_obj (other.m_obj)
+ {
+ other.m_obj = NULL;
+ }
+
+ /* Destroy this instance. */
+ ~ref_ptr ()
+ {
+ if (m_obj != NULL)
+ Policy::decref (m_obj);
+ }
+
+ /* Copy another instance. */
+ template<typename U,
+ typename = std::is_convertible<U *, T*>>
+ ref_ptr &operator= (const ref_ptr<U, Policy> &other)
+ {
+ /* Note that self-assignment is not checked here, as it isn't
+ possible: self-assignments will choose the non-template
+ function. */
+ reset (other.m_obj);
+ if (m_obj != NULL)
+ Policy::incref (m_obj);
+ return *this;
+ }
+
+ ref_ptr &operator= (const ref_ptr &other)
+ {
+ /* Do nothing on self-assignment. */
+ if (this != &other)
+ {
+ reset (other.m_obj);
+ if (m_obj != NULL)
+ Policy::incref (m_obj);
+ }
+ return *this;
+ }
+
+ /* Transfer ownership from OTHER. */
+ template<typename U,
+ typename = std::is_convertible<U *, T*>>
+ ref_ptr &operator= (ref_ptr<U, Policy> &&other)
+ {
+ /* Note that self-assignment is not checked here, as it isn't
+ possible: self-assignments will choose the non-template
+ function. */
+ reset (other.m_obj);
+ other.m_obj = NULL;
+ return *this;
+ }
+
+ ref_ptr &operator= (ref_ptr &&other)
+ {
+ /* Do nothing on self-assignment. */
+ if (this != &other)
+ {
+ reset (other.m_obj);
+ other.m_obj = NULL;
+ }
+ return *this;
+ }
+
+ /* Change this instance's referent. OBJ is a reference, management
+ of which is now transferred to this class. */
+ void reset (T *obj)
+ {
+ if (m_obj != NULL)
+ Policy::decref (m_obj);
+ m_obj = obj;
+ }
+
+ operator bool () const noexcept
+ {
+ return m_obj != nullptr;
+ }
+
+ /* Return this instance's referent without changing the state of
+ this class. */
+ T *get () const noexcept
+ {
+ return m_obj;
+ }
+
+ /* Return this instance's referent, and stop managing this
+ reference. The caller is now responsible for the ownership of
+ the reference. */
+ ATTRIBUTE_UNUSED_RESULT T *release () noexcept
+ {
+ T *result = m_obj;
+
+ m_obj = NULL;
+ return result;
+ }
+
+ /* Let users refer to members of the underlying pointer. */
+ T *operator-> () const noexcept
+ {
+ return m_obj;
+ }
+
+ /* Acquire a new reference and return a ref_ptr that owns it. */
+ template <class TObj>
+ static ref_ptr<T, Policy> new_reference (TObj *obj)
+ {
+ Policy::incref (obj);
+ if constexpr (std::is_base_of<T, TObj>::value
+ || std::is_base_of<TObj, T>::value)
+ return ref_ptr<T, Policy> (static_cast<T *> (obj));
+ else
+ return ref_ptr<T, Policy> (obj);
+ }
+
+ template<typename P = Policy, typename = std::enable_if_t<
+ std::is_same<P, gdbpy_ref_policy>::value>>
+ int visit (typename P::visitproc visit, void *arg) noexcept
+ {
+ return P::visit (m_obj, visit, arg);
+ }
+
+ template<typename P = Policy, typename = std::enable_if_t<
+ std::is_same<P, gdbpy_ref_policy>::value>>
+ void clear (P * = nullptr) noexcept
+ {
+ P::clear (&m_obj);
+ }
+
+ private:
+
+ T *m_obj;
+};
+
+template<typename T, typename U, typename Policy>
+inline bool operator== (const ref_ptr<T, Policy> &lhs,
+ const ref_ptr<U, Policy> &rhs)
+{
+ return lhs.get () == rhs.get ();
+}
+
+template<typename T, typename U, typename Policy>
+inline bool operator== (const ref_ptr<T, Policy> &lhs, const U *rhs)
+{
+ return lhs.get () == rhs;
+}
+
+template<typename T, typename Policy>
+inline bool operator== (const ref_ptr<T, Policy> &lhs, const std::nullptr_t)
+{
+ return lhs.get () == nullptr;
+}
+
+template<typename T, typename U, typename Policy>
+inline bool operator== (const T *lhs, const ref_ptr<U, Policy> &rhs)
+{
+ return lhs == rhs.get ();
+}
+
+template<typename T, typename Policy>
+inline bool operator== (const std::nullptr_t, const ref_ptr<T, Policy> &rhs)
+{
+ return nullptr == rhs.get ();
+}
+
+template<typename T, typename U, typename Policy>
+inline bool operator!= (const ref_ptr<T, Policy> &lhs,
+ const ref_ptr<U, Policy> &rhs)
+{
+ return lhs.get () != rhs.get ();
+}
+
+template<typename T, typename U, typename Policy>
+inline bool operator!= (const ref_ptr<T, Policy> &lhs, const U *rhs)
+{
+ return lhs.get () != rhs;
+}
+
+template<typename T, typename Policy>
+inline bool operator!= (const ref_ptr<T, Policy> &lhs, const std::nullptr_t)
+{
+ return lhs.get () != nullptr;
+}
+
+template<typename T, typename U, typename Policy>
+inline bool operator!= (const T *lhs, const ref_ptr<U, Policy> &rhs)
+{
+ return lhs != rhs.get ();
+}
+
+template<typename T, typename Policy>
+inline bool operator!= (const std::nullptr_t, const ref_ptr<T, Policy> &rhs)
+{
+ return nullptr != rhs.get ();
+}
+
+}
+
+#endif /* GDBSUPPORT_GDB_REF_PTR_H */
diff --git a/include/py-obj-dict.hpp b/include/py-obj-dict.hpp
new file mode 100644
index 0000000..42154ba
--- /dev/null
+++ b/include/py-obj-dict.hpp
@@ -0,0 +1,189 @@
+#pragma once
+
+#include "py-obj-type.hpp"
+
+/* Helpers for Python extension objects that have a __dict__ attribute.
+
+ Any Python C object extension (for instance, a class A inheriting PyObject)
+ needing __dict__ should own an attribute GDBPYOBJECT_DICT_ATTR (A).
+
+ Access to the dict requires a custom getter defined via PyGetSetDef.
+ gdb_PyGetSetDef my_object_getset[] =
+ {
+ gdbpy_dict_attr_getter (A, "object"),
+ ...
+ { nullptr }
+ };
+
+ It is also important to note that __dict__ is used during the attribute
+ look-up. Since this dictionary is not managed by Python and is not exposed
+ via tp_dictoffset, custom attribute getter (tp_getattro) and setter
+ (tp_setattro) are required to correctly redirect attribute access to the
+ dictionary:
+ PyType_Slot object_type_slot[] = {
+ ...,
+ gdbpy_dict_attr_PyType_Slots (A),
+ ...
+ };
+
+ Finally, heap-allocated types owning a __dict__ should visit and clear their
+ __dict__.
+ static int
+ A_tp_traverse (PyObject *self, visitproc visit, void *arg)
+ {
+ auto *obj = (A *) self;
+ gdbpy_dict_attr_Py_VISIT (obj);
+ ...
+ return 0;
+ }
+
+ static void
+ A_tp_clear (PyObject *self)
+ {
+ auto *obj = (A *) self;
+ gdbpy_dict_attr_Py_CLEAR (obj);
+ ...
+ }
+ */
+template <class T>
+struct gdbpy_dict_helpers
+{
+private:
+
+ /* Compute the address of the __dict__ attribute for the given PyObject. */
+ static PyObject **compute_addr (PyObject *self)
+ {
+ auto *dict_owner = reinterpret_cast<T *> (self);
+ return &dict_owner->dict;
+ }
+
+public:
+
+ static PyObject *
+ allocate (PyObject *self)
+ {
+ PyObject **py_dict_ptr = gdbpy_dict_helpers::compute_addr (self);
+ PyObject *py_dict = *py_dict_ptr;
+ gdb_assert (py_dict == nullptr);
+ *py_dict_ptr = PyDict_New ();
+ py_dict = *py_dict_ptr;
+ return py_dict;
+ }
+
+
+ /* Generic implementation of the getter for the __dict__ attribute for objects
+ having a dictionary. The CLOSURE argument is unused. */
+
+ static PyObject *
+ getter (PyObject *self, void *closure ATTRIBUTE_UNUSED)
+ {
+ PyObject **py_dict_ptr = gdbpy_dict_helpers::compute_addr (self);
+ PyObject *py_dict = *py_dict_ptr;
+ if (py_dict == nullptr)
+ {
+ PyErr_SetString (PyExc_AttributeError,
+ "This object has no __dict__");
+ return nullptr;
+ }
+ //if (py_dict == nullptr)
+ // py_dict = gdbpy_dict_helpers::allocate (self, py_dict_ptr);
+ return Py_NewRef (py_dict);
+ }
+
+ /* Generic attribute getter function similar to PyObject_GenericGetAttr () but
+ that should be used when the object has a dictionary __dict__. */
+ static PyObject *
+ getattro (PyObject *self, PyObject *attr)
+ {
+ PyObject *value = PyObject_GenericGetAttr (self, attr);
+ if (value != nullptr)
+ return value;
+
+ if (! PyErr_ExceptionMatches (PyExc_AttributeError))
+ return nullptr;
+
+ gdbpy_ref<> dict (gdbpy_dict_helpers::getter (self, nullptr));
+ if (dict == nullptr)
+ return nullptr;
+
+ /* Clear previous AttributeError set by PyObject_GenericGetAttr when it
+ did not find the attribute, and try to get the attribute from __dict__. */
+ PyErr_Clear();
+
+ value = PyDict_GetItemWithError (dict.get (), attr);
+ if (value != nullptr)
+ return Py_NewRef (value);
+
+ /* If PyDict_GetItemWithError() returns NULL because an error occurred, it
+ sets an exception. Propagate it by returning NULL. */
+ if (PyErr_Occurred () != nullptr)
+ return nullptr;
+
+ /* If the key is not found, PyDict_GetItemWithError() returns NULL without
+ setting an exception. Failing to set one here would later result in:
+ <class 'SystemError'>: error return without exception set
+ Therefore, we must explicitly raise an AttributeError in this case. */
+ PyErr_Format (PyExc_AttributeError,
+ "'%s' object has no attribute '%s'",
+ gdbpy_py_obj_tp_name (self),
+ PyUnicode_AsUTF8AndSize (attr, nullptr));
+ return nullptr;
+ }
+
+ /* Generic attribute setter function similar to PyObject_GenericSetAttr () but
+ that should be used when the object has a dictionary __dict__. */
+ static int
+ setattro (PyObject *self, PyObject *attr, PyObject *value)
+ {
+ if (PyObject_GenericSetAttr (self, attr, value) == 0)
+ return 0;
+
+ if (! PyErr_ExceptionMatches (PyExc_AttributeError))
+ return -1;
+
+ gdbpy_ref<> dict (gdbpy_dict_helpers::getter (self, nullptr));
+ if (dict == nullptr)
+ return -1;
+
+ /* Clear previous AttributeError set by PyObject_GenericGetAttr() when it
+ did not find the attribute, and try to set the attribute into __dict__. */
+ PyErr_Clear();
+
+ /* Set the new value.
+ Note: the old value is managed by PyDict_SetItem(), so no need to get
+ a borrowed reference on it and decrement its reference counter before
+ setting a new value. */
+ return PyDict_SetItem (dict.get (), attr, value);
+ }
+};
+
+#define GDBPYOBJECT_DICT_ATTR(O) \
+ PyObject *dict; \
+ using dict_attr = gdbpy_dict_helpers<O>
+
+#define gdbpy_dict_attr_getter(O, object_name) \
+ { \
+ "__dict__", /* name */ \
+ (getter) O::dict_attr::getter, \
+ (setter) nullptr, \
+ "The __dict__ for this " object_name ".", /* doc */ \
+ nullptr, /* closure */ \
+ }
+
+#define gdbpy_dict_attr_PyType_Slots(O) \
+ {Py_tp_getattro, reinterpret_cast<void *>(O::dict_attr::getattro)}, \
+ {Py_tp_setattro, reinterpret_cast<void *>(O::dict_attr::setattro)}
+
+#define gdbpy_dict_attr_Py_ALLOC(T, self)\
+ gdbpy_dict_helpers<T>::allocate (self)
+
+#define gdbpy_dict_attr_Py_VISIT(obj) \
+ printf ("visiting __dict__\n"); \
+ Py_VISIT (obj->dict)
+
+#define gdbpy_dict_attr_Py_CLEAR(obj) \
+ printf ("clearing __dict__\n"); \
+ Py_CLEAR (obj->dict)
+
+#define gdbpy_dict_attr_Py_ALLOC(T, self) \
+ gdbpy_dict_helpers<T>::allocate (self)
diff --git a/include/py-obj-type.hpp b/include/py-obj-type.hpp
new file mode 100644
index 0000000..0762519
--- /dev/null
+++ b/include/py-obj-type.hpp
@@ -0,0 +1,424 @@
+/* Helpers related to Python object type
+
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDB_PYTHON_PY_OBJ_TYPE_H
+#define GDB_PYTHON_PY_OBJ_TYPE_H
+
+#include "py-ref.hpp"
+#include <cstdio>
+#include <string>
+
+/* Return the type's fully qualified name from a PyTypeObject. */
+extern std::string gdb_py_tp_name (PyTypeObject *py_type) noexcept;
+
+/* Return the type's fully qualified name from a PyObject. */
+extern std::string gdbpy_py_obj_tp_name (PyObject *self) noexcept;
+
+using gdbpy_bref_pytype = gdbpy_borrowed_ref<PyTypeObject>;
+
+namespace gdbpy {
+ template <typename T>
+ struct is_pyobject_wrapper: std::false_type {};
+
+ template <typename T>
+ struct is_pyobject_wrapper<gdbpy_ref <T>>: std::true_type {};
+
+ template <typename T>
+ struct is_pyobject_wrapper<gdbpy_borrowed_ref <T>>: std::true_type {};
+
+ template <typename T>
+ constexpr bool is_pyobject_wrapper_v = is_pyobject_wrapper<T>::value;
+} // namespace gdbpy
+
+template <class TypeWrapper>
+struct gdbpy_object_type
+{
+private:
+ PyTypeObject *pytype_ () const noexcept
+ {
+ return ((TypeWrapper *) this)->pytype ();
+ }
+
+public:
+
+ operator PyTypeObject *() const noexcept
+ {
+ return this->pytype_ ();
+ }
+
+ std::string tp_name () const noexcept
+ {
+ return gdb_py_tp_name (this->pytype_ ());
+ }
+
+ bool operator== (const TypeWrapper &other) const noexcept
+ {
+ return this->pytype_ () == other.pytype_ ();
+ }
+
+ bool same_as (PyTypeObject *objtype) const noexcept
+ {
+ return objtype == this->pytype_ ();
+ }
+
+ bool same_as (PyObject *obj) const noexcept
+ {
+ /* Note: if the type objects are the same, their address should be
+ the same because GDB manages all those types as singletons. */
+ return (obj != nullptr
+ && ((PyType_Check (obj) != 0
+ && this->same_as (reinterpret_cast <PyTypeObject *> (obj)))
+ || this->same_as (Py_TYPE (obj))))
+ || this->pytype_ () == nullptr;
+ }
+
+ bool same_as (std::nullptr_t) const noexcept
+ {
+ return this->pytype_ () == nullptr;
+ }
+
+ bool same_as (const gdbpy_borrowed_ref <PyTypeObject> &wrapper) const noexcept
+ {
+ return this->pytype_ () == wrapper.get ();
+ }
+
+ template <typename T>
+ bool same_as (const gdbpy_ref <T> &wrapper) const noexcept
+ {
+ return this->same_as (static_cast <PyObject *> (wrapper.get ()));
+ }
+
+ void *slot (int slot_id) const noexcept
+ {
+ return PyType_GetSlot (this->pytype_ (), slot_id);
+ }
+
+ gdbpy_borrowed_ref <PyTypeObject> base_type () const noexcept
+ {
+ return gdbpy_borrowed_ref <PyTypeObject>
+ ((PyTypeObject *) this->slot (Py_tp_base));
+ }
+
+ bool heap_type () const noexcept
+ {
+ return PyType_GetFlags (this->pytype_ ()) | Py_TPFLAGS_HEAPTYPE;
+ }
+};
+
+struct gdbpy_object_type_ref:
+ public gdbpy_object_type <gdbpy_object_type_ref>,
+ public gdbpy_ref <>
+{
+ gdbpy_object_type_ref () noexcept = default;
+ gdbpy_object_type_ref (gdbpy_object_type_ref &&other) noexcept = default;
+ gdbpy_object_type_ref& operator= (gdbpy_object_type_ref &&other) noexcept = default;
+
+ gdbpy_object_type_ref (std::nullptr_t) noexcept
+ : gdbpy_ref<> (nullptr)
+ {
+ }
+
+ gdbpy_object_type_ref (gdbpy_ref <> &&other) noexcept
+ : gdbpy_ref<> (std::move(other))
+ {
+ gdb_assert (PyType_Check (this->get ()) != 0);
+ }
+
+ PyTypeObject *pytype () const noexcept
+ {
+ return reinterpret_cast <PyTypeObject *> (this->get ());
+ }
+};
+
+struct gdbpy_object_type_bref:
+ public gdbpy_object_type <gdbpy_object_type_bref>,
+ public gdbpy_borrowed_ref <>
+{
+ gdbpy_object_type_bref () noexcept = default;
+ gdbpy_object_type_bref (const gdbpy_object_type_bref &other) noexcept = default;
+ gdbpy_object_type_bref& operator= (const gdbpy_object_type_bref &other) noexcept = default;
+
+ gdbpy_object_type_bref (const gdbpy_object_type_ref& other) noexcept
+ : gdbpy_borrowed_ref<> (other.get ())
+ {
+ }
+
+ gdbpy_object_type_bref (std::nullptr_t) noexcept
+ : gdbpy_borrowed_ref<> (nullptr)
+ {
+ }
+
+ PyTypeObject *pytype () const noexcept
+ {
+ if (this->get () == nullptr)
+ return nullptr;
+ return reinterpret_cast <PyTypeObject *> (this->get ());
+ }
+};
+
+struct gdbpy_py_obj_type_bref:
+ public gdbpy_object_type <gdbpy_py_obj_type_bref>,
+ public gdbpy_borrowed_ref <PyTypeObject>
+{
+ gdbpy_py_obj_type_bref () noexcept = default;
+ gdbpy_py_obj_type_bref (const gdbpy_py_obj_type_bref &other) noexcept = default;
+ gdbpy_py_obj_type_bref& operator= (gdbpy_py_obj_type_bref &&other) noexcept = default;
+
+ gdbpy_py_obj_type_bref (const gdbpy_borrowed_ref <PyTypeObject> &pytype) noexcept
+ : gdbpy_borrowed_ref<PyTypeObject> (pytype)
+ {
+ }
+
+ gdbpy_py_obj_type_bref (PyTypeObject *pytype) noexcept
+ : gdbpy_borrowed_ref<PyTypeObject> (pytype)
+ {
+ }
+
+ PyTypeObject *pytype () const noexcept
+ {
+ return this->get ();
+ }
+};
+
+/* Wrapper around PyObject_TypeCheck().
+ Note: this wrapper will be decommissioned once all the Python extension
+ types will have been migrated to the Python limited API. */
+inline bool
+gdbpy_type_check (PyObject *obj, const PyTypeObject &obj_type)
+{
+ return PyObject_TypeCheck (obj, const_cast<PyTypeObject *> (&obj_type));
+}
+
+/* Wrapper around PyObject_TypeCheck() to compare against any wrapper
+ convertible to 'PyTypeObject *'. */
+template <class OType,
+ typename = std::is_convertible<OType, PyTypeObject *>>
+inline bool
+gdbpy_type_check (PyObject *obj, const OType &obj_type)
+{
+ return PyObject_TypeCheck (obj, obj_type.pytype ());
+}
+
+/* Wrapper around PyObject_TypeCheck() similar to the previous one, but
+ accepting 'gdbpy_ref<T>' and 'gdbpy_borrowed_ref' instead of raw
+ 'PyObject *'. */
+template <class Wrapper,
+ class OType,
+ typename = gdbpy::is_pyobject_wrapper<Wrapper>,
+ typename = std::is_convertible<OType, PyTypeObject *>>
+inline bool
+gdbpy_type_check (const Wrapper &obj, const OType &obj_type)
+{
+ return PyObject_TypeCheck (obj.get (), obj_type.pytype ());
+}
+
+struct gdbpy_heap_type
+{
+ using tp_traverse_t = int (*) (PyObject *self, visitproc visit, void *arg);
+ using tp_clear_t = void (*) (PyObject *self);
+
+ static constexpr unsigned long
+ tp_flags_default ()
+ { return Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC; }
+
+ static int
+ tp_traverse (PyObject *self, visitproc visit, void *arg,
+ const gdbpy_object_type_bref &base_type,
+ tp_traverse_t tp_traverse_impl)
+ {
+ int res = 0;
+
+ /* Traverse the object's attributes. */
+ if (tp_traverse_impl != nullptr)
+ {
+ if ((res = tp_traverse_impl (self, visit, arg)) != 0)
+ return res;
+ }
+
+ /* Don't forget to call tp_traverse of the base class. */
+ if (base_type != nullptr)
+ {
+ auto base_tp_traverse
+ = (gdbpy_heap_type::tp_traverse_t) base_type.slot (Py_tp_traverse);
+ if ((res = base_tp_traverse (self, visit, arg)) != 0)
+ return res;
+ }
+
+ /* From Python 3.9, tp_traverse must visit the type for heap-allocated
+ types. Older versions must not visit it.
+ Note that, in case of inheritance, the type should be visited only
+ once. */
+#if PY_VERSION_HEX >= 0x03090000
+ if (base_type == nullptr)
+ {
+ printf ("calling Py_VISIT (Py_TYPE (self))\n");
+ Py_VISIT (Py_TYPE (self));
+ }
+#endif
+
+ return res;
+ }
+
+ static void
+ tp_clear (PyObject *self,
+ const gdbpy_object_type_bref &base_type,
+ tp_clear_t tp_clear_impl)
+ {
+ /* Clear any fields specific to the type. */
+ if (tp_clear_impl != nullptr)
+ tp_clear_impl (self);
+
+ /* Don't forget to call tp_clear of the base class. */
+ if (base_type != nullptr)
+ {
+ auto base_tp_clear
+ = (gdbpy_heap_type::tp_clear_t) base_type.slot (Py_tp_clear);
+ base_tp_clear (self);
+ }
+ }
+
+ static void
+ tp_dealloc (PyObject *self)
+ {
+ auto *tp = Py_TYPE (self);
+
+ /* Call PyObject_GC_UnTrack() before any fields are invalidated. */
+ PyObject_GC_UnTrack (self);
+
+ /* Clear the object's attributes. */
+ auto self_tp_clear = (tp_clear_t) PyType_GetSlot (tp, Py_tp_clear);
+ if (self_tp_clear)
+ self_tp_clear (self);
+
+ /* Delete the object. */
+ PyObject_GC_Del (self);
+
+ /* Decrement the reference count of the type. */
+ printf ("decrement ref count on Py_TYPE (self)\n");
+ Py_DECREF (tp);
+ }
+
+ /* A convenient wrapper enforcing the call to PyObject_GC_Track() on a newly
+ allocated Python object before it goes out of scope. If a developer
+ forgets to call track(), the destructor raises a runtime assertion in the
+ destructor. */
+ template <class T>
+ struct auto_gc_track
+ : private gdbpy_ref<T>
+ {
+ auto_gc_track (T *obj)
+ : gdbpy_ref<T> (obj)
+ {}
+
+ auto_gc_track (auto_gc_track &&other)
+ : gdbpy_ref<T> (std::move (other))
+ {}
+
+ auto_gc_track &operator= (auto_gc_track &&other)
+ {
+ if (this != other)
+ gdbpy_ref<T>::operator= (std::move (other));
+ return *this;
+ }
+
+ gdbpy_borrowed_ref<T> borrowed_ref () const noexcept
+ {
+ return gdbpy_ref<T>::get ();
+ }
+
+ gdbpy_ref<T> strong_ref () const noexcept
+ {
+ return gdbpy_ref<T>(*this);
+ }
+
+ /* Start tracking of the newly allocated object with the GC.
+ Return gdbpy_ref<T> as the finalized object. */
+ gdbpy_ref<T> track ()
+ {
+ PyObject_GC_Track (this->get ());
+ return std::move (*this);
+ }
+
+ /* Bail out, i.e. free the newly allocated object and return NULL. */
+ std::nullptr_t bailout ()
+ {
+ auto obj = this->track ();
+ return nullptr;
+ }
+
+ inline explicit operator bool () const noexcept
+ {
+ return gdbpy_ref<T>::get () != nullptr;
+ }
+
+ inline bool operator== (const std::nullptr_t) const noexcept
+ {
+ return gdbpy_ref<T>::get () == nullptr;
+ }
+
+ T *operator-> () const noexcept
+ {
+ return gdbpy_ref<T>::get ();
+ }
+
+ ~auto_gc_track ()
+ {
+ /* The object should have been tracked by the GC, and released before
+ this destructor is called. */
+ gdb_assert (gdbpy_ref<T>::get () == nullptr);
+ }
+ };
+
+ /* Convenient wrapper to PyObject_GC_New() returning an auto_gc_track<T>
+ object, used to enforce the call to PyObject_GC_Track() on the newly
+ allocated Python object before auto_gc_track<T> goes out of scope. */
+ template <class T>
+ static auto_gc_track<T>
+ make_pyobj (const gdbpy_object_type_bref &obj_type)
+ {
+ auto_gc_track<T> obj
+ (static_cast<T *> (PyObject_GC_New (T, obj_type.pytype ())));
+ return obj;
+ }
+};
+
+/* Helper macro to initialize mandatory slot IDs (i.e. Py_tp_traverse,
+ Py_tp_clear, and Py_tp_dealloc) for garbage-collected types, using
+ default methods.
+ Note: those defaults are ideal for classes containing no strong
+ references of Python objects in their attributes. */
+
+/* Helper macro to initialize mandatory slot IDs (i.e. Py_tp_traverse,
+ Py_tp_clear, and Py_tp_dealloc) for garbage-collected types, providing
+ custom methods for tp_traverse and tp_clear. */
+#define gdbpy_heap_type_methods(fn_traverse, fn_clear) \
+ {Py_tp_traverse, (void *) fn_traverse}, \
+ {Py_tp_clear, (void *) fn_clear}, \
+ {Py_tp_dealloc, (void *) gdbpy_heap_type::tp_dealloc}
+
+/* Helper macro declaring 'varname', creating a new heap-allocated object of
+ static type OBJTYPE, initializing it following specs declared in py_objtype
+ (the dynamically-allocated type corresponding to OBJTYPE), and validating
+ its creation or bailing out. */
+#define gdbpy_heap_type_allocate_or_bailout(varname, OBJTYPE, py_objtype)\
+ auto varname = gdbpy_heap_type::make_pyobj<OBJTYPE> (py_objtype); \
+ if (varname == nullptr) \
+ return nullptr
+
+#endif /* GDB_PYTHON_PY_OBJ_TYPE_H */
diff --git a/include/py-ref.hpp b/include/py-ref.hpp
new file mode 100644
index 0000000..c8ea3a0
--- /dev/null
+++ b/include/py-ref.hpp
@@ -0,0 +1,204 @@
+/* Python reference-holding class
+
+ Copyright (C) 2016-2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDB_PYTHON_PY_REF_H
+#define GDB_PYTHON_PY_REF_H
+
+#include "gdb_ref_ptr.hpp"
+#include <Python.h>
+#include "python-traits.hpp"
+
+/* A policy class for gdb::ref_ptr for Python reference counting. */
+struct gdbpy_ref_policy
+{
+ static void incref (PyObject *ptr)
+ {
+ Py_INCREF (ptr);
+ }
+
+ static void decref (PyObject *ptr)
+ {
+ Py_DECREF (ptr);
+ }
+
+ using visitproc = int (*)(PyObject *object, void *arg);
+
+ static int visit (PyObject *ptr, visitproc visit, void *arg) noexcept
+ {
+ Py_VISIT (ptr);
+ return 0;
+ }
+
+ template <typename T>
+ static void clear (T **ptr) noexcept
+ {
+ Py_CLEAR (*ptr);
+ }
+};
+
+/* Helper macro to allocate and build a new gdbpy_ref<T> from a pointer
+ while incrementing the ref counter. */
+#define gdbpy_ref_NEWREF(T, ptr_obj) \
+ new gdbpy_ref<T> (gdbpy_ref<T>::new_reference (ptr_obj))
+
+/* Cousin macro of Py_VISIT specialized for gdbpy_ref<T>.
+ Note: this macro has to be used inside the custom tp_traverse function for
+ type T so that its parameters visit and arg are known. */
+#define gdbpy_ref_VISIT(ptr_gdbpy_ref) \
+ do \
+ { \
+ if (ptr_gdbpy_ref) \
+ { \
+ int ret \
+ = (ptr_gdbpy_ref)->visit (visit, arg); \
+ if (ret) \
+ return ret; \
+ } \
+ } \
+ while (0)
+
+/* Cousin macro of Py_CLEAR specialized for gdbpy_ref<T>. */
+#define gdbpy_ref_CLEAR(obj) \
+ do \
+ { \
+ if (obj) \
+ { \
+ (obj)->clear (); \
+ delete (obj); \
+ (obj) = nullptr; \
+ } \
+ } \
+ while (0)
+
+/* A gdb::ref_ptr that has been specialized for Python objects or
+ their "subclasses". */
+template<typename T = PyObject> using gdbpy_ref
+ = gdb::ref_ptr<T, gdbpy_ref_policy>;
+
+/* A class representing a borrowed reference.
+
+ This is a simple wrapper for a PyObject*. Aside from documenting
+ what the code does, the main advantage of using this is that
+ conversion to a gdbpy_ref<> is guaranteed to make a new
+ reference. */
+template <class T = PyObject>
+class gdbpy_borrowed_ref
+{
+public:
+
+ gdbpy_borrowed_ref () noexcept
+ : m_obj (nullptr)
+ {
+ }
+
+ gdbpy_borrowed_ref (const std::nullptr_t) noexcept
+ : m_obj (nullptr)
+ {
+ }
+
+ template <typename U,
+ typename = std::is_convertible<U *, T*>>
+ gdbpy_borrowed_ref (U *obj) noexcept
+ : m_obj (obj)
+ {
+ }
+
+ template <typename U,
+ typename = std::is_convertible<U *, T*>>
+ gdbpy_borrowed_ref (const gdbpy_ref<U> &ref) noexcept
+ : m_obj (ref.get ())
+ {
+ }
+
+ gdbpy_borrowed_ref (const gdbpy_borrowed_ref &other) noexcept
+ : m_obj (other.m_obj)
+ {
+ }
+
+ gdbpy_borrowed_ref &operator= (const gdbpy_borrowed_ref &other)
+ {
+ m_obj = other.m_obj;
+ return *this;
+ }
+
+ operator bool () const noexcept
+ {
+ return m_obj != nullptr;
+ }
+
+ operator T * () const noexcept
+ {
+ return m_obj;
+ }
+
+ /* When converting a borrowed reference to a gdbpy_ref<>, a new
+ reference is acquired. */
+ template <typename TObj,
+ typename = std::is_convertible<TObj *, T *>>
+ operator gdbpy_ref<TObj> ()
+ {
+ gdb_assert (m_obj != nullptr);
+ return gdbpy_ref<TObj>::new_reference (m_obj);
+ }
+
+ gdbpy_ref<T> strong_ref () const noexcept
+ {
+ return static_cast<gdbpy_ref<T>> (*this);
+ }
+
+ /* Let users refer to members of the underlying pointer. */
+ T *operator-> () const noexcept
+ {
+ return m_obj;
+ }
+
+ T *get () const noexcept
+ {
+ return m_obj;
+ }
+
+private:
+ T *m_obj;
+};
+
+template<typename T>
+inline bool operator== (const gdbpy_borrowed_ref<T> &lhs, const std::nullptr_t)
+{
+ return lhs.get () == nullptr;
+}
+
+template<typename T>
+inline bool operator!= (const gdbpy_borrowed_ref<T> &lhs, const std::nullptr_t)
+{
+ return lhs.get () != nullptr;
+}
+
+template<typename T>
+inline bool operator== (const std::nullptr_t, const gdbpy_borrowed_ref<T> &rhs)
+{
+ return nullptr == rhs.get ();
+}
+
+template<typename T>
+inline bool operator!= (const std::nullptr_t, const gdbpy_borrowed_ref<T> &rhs)
+{
+ return nullptr != rhs.get ();
+}
+
+#endif /* GDB_PYTHON_PY_REF_H */
diff --git a/include/python-internal.hpp b/include/python-internal.hpp
new file mode 100644
index 0000000..151ad47
--- /dev/null
+++ b/include/python-internal.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <Python.h>
+
+#include "gdb-others.hpp"
+#include "py-ref.hpp"
+#include "py-obj-dict.hpp"
+#include "py-obj-type.hpp"
+
+extern gdbpy_object_type_ref
+gdbpy_type_initialize (gdbpy_borrowed_ref<> &mod,
+ PyType_Spec *spec,
+ PyObject *bases = nullptr,
+ bool disallow_instantiation = false);
diff --git a/include/python-traits.hpp b/include/python-traits.hpp
new file mode 100644
index 0000000..d1d3053
--- /dev/null
+++ b/include/python-traits.hpp
@@ -0,0 +1,93 @@
+/* Type traits relating to GDB's Python integration.
+
+ Copyright (C) 2008-2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef GDB_PYTHON_PYTHON_TRAITS_H
+#define GDB_PYTHON_PYTHON_TRAITS_H
+
+namespace gdb
+{
+/* All of our custom Python types are created as structs, like this:
+
+ struct some_new_type : public PyObject
+ {
+ ... various fields ...
+ };
+
+ Then instances of this struct are created by calling PyObject_New,
+ either directly within GDB's C++ code, or within Python when a user's
+ Python script creates an instance of that class.
+
+ The problem is that Python is written in C, and PyObject_New doesn't
+ call any constructors for `some_new_type`, nor for any of the fields
+ within `some_new_type`.
+
+ If `some_new_type` is Plain Old Data (POD), then this is fine. Or, to
+ be more C++ specific, if `some_new_type` is trivially default
+ constructable, then we're fine.
+
+ But if a field within `some_new_type` has a non-trivial constructor,
+ then we're in trouble as that constructor will never be run.
+
+ An example of a problematic field type is frame_info_ptr. The
+ constructor for this type registers the new object with a central
+ management object, recording the `this` pointer, using this type within
+ `some_new_type` will not work as expected; frame invalidation will not
+ show up within the frame_info_ptr as you might expect.
+
+ And so, this type trait exists. Whenever a struct is created to define
+ a new Python type we should add a line like:
+
+ static_assert (gdb::is_python_allocatable_v<some_new_type>);
+
+ This will fail if any field of `some_new_type` is unsuitable for this
+ use.
+
+ We don't actually check is_trivially_default_constructible here. Some
+ types, e.g. ui_file_style::color, have non-trivial (or no default)
+ constructors, but are still safe to use within `some_new_type` because
+ their constructors just initialise data fields; there's nothing
+ "special" that the constructor does that cannot be achieved by
+ assigning the fields after creation with PyObject_New.
+
+ What actually matters is that the type is trivially destructible
+ (Python won't call C++ destructors, so destructors with side effects,
+ like deregistering from a list, would be skipped) and trivially
+ copyable (Python may copy objects with memcpy). Types like
+ frame_info_ptr, whose constructors and destructors have side effects
+ such as registering with a central management object, will be caught
+ because they are neither trivially destructible nor trivially copyable.
+ Types like ui_file_style are trivially destructible and copyable, so
+ pass this trait. */
+
+template<typename T>
+struct is_python_allocatable
+{
+ static constexpr bool value =
+ std::is_trivially_destructible_v<T>
+ && std::is_trivially_copyable_v<T>;
+};
+
+/* Helper for the above trait to make it more usable. */
+
+template<typename T>
+inline constexpr bool is_python_allocatable_v
+ = is_python_allocatable<T>::value;
+}
+
+#endif /* GDB_PYTHON_PYTHON_TRAITS_H */
diff --git a/pydbg.cpp b/pydbg.cpp
new file mode 100644
index 0000000..bcfae2e
--- /dev/null
+++ b/pydbg.cpp
@@ -0,0 +1,80 @@
+#include <cstddef>
+#include <iostream>
+
+#include <Python.h>
+
+int main(int argc, char *argv[], char *envp[])
+{
+ Py_Initialize();
+
+ PyObject *pmodule = PyImport_ImportModule("MyModule");
+ if (!pmodule) {
+ PyErr_Print();
+ std::cerr << "Failed to import module MyModule" << std::endl;
+ return -1;
+ }
+
+ PyObject *classA = PyObject_GetAttrString(pmodule, "A");
+ if (!classA) {
+ std::cerr << "Unable to get type A from MyModule" << std::endl;
+ return -1;
+ }
+
+ PyObject *classB = PyObject_GetAttrString(pmodule, "B");
+ if (!classB) {
+ std::cerr << "Unable to get type B from MyModule" << std::endl;
+ return -1;
+ }
+
+ PyObject *myAInstance = PyObject_CallObject(classA, NULL);
+ if (!myAInstance) {
+ std::cerr << "Instantioation of MyClass failed" << std::endl;
+ return -1;
+ }
+ Py_DECREF(myAInstance);
+
+ PyObject *pGlobal = PyDict_New();
+ PyObject *pValue = PyRun_String(
+ "import MyModule as MM\n"
+ "b = MM.B()\n"
+ "b.foo = 1234\n"
+ "print(b.name(), b.iamb())\n"
+ "\n"
+ "class C(MM.B):\n"
+ " pass\n"
+ "c = C()\n"
+ "c.foo = 1234\n"
+ "print(dir(c))\n"
+ "print(c.name(), c.iamb())\n"
+ "\n"
+ "class D(MM.A):\n"
+ " def __init__(self):\n"
+ " super().__init__()\n"
+ " self._cycle = self\n"
+ "\n"
+ "d = D()\n"
+ "d.foo = 1234\n"
+ "print(dir(d))\n"
+ "print(d.name(), d.iama())\n"
+ "\n"
+ "class E(MM.B):\n"
+ " def __init__(self):\n"
+ " super().__init__()\n"
+ " self._cycle = self\n"
+ "\n"
+ "e = E()\n"
+ "e.foo = 1234\n"
+ "print(dir(e))\n"
+ "print(e.name(), e.iama(), e.iamb())\n"
+ "\n"
+ "f = MM.ObjWithAlloc()\n"
+ "print('{} {}'.format(f.name(), f.whoami()))\n"
+ "print(f.speak())\n"
+ , Py_file_input, pGlobal, pGlobal);
+ Py_XDECREF(pValue);
+ Py_DECREF(pGlobal);
+
+ Py_Finalize();
+
+ return 0;
+}
diff --git a/src/A.cpp b/src/A.cpp
new file mode 100644
index 0000000..c2738a5
--- /dev/null
+++ b/src/A.cpp
@@ -0,0 +1,113 @@
+#include "my_py_module.hpp"
+
+namespace {
+ extern PyType_Spec A_object_type_spec;
+} /* namespace anonymous */
+
+namespace {
+
+static int
+A_init (PyObject *self, PyObject *args, PyObject *kwds)
+{
+ auto* obj = (A *) self;
+ gdbpy_dict_attr_Py_ALLOC (A, self);
+ obj->m_a = PyUnicode_FromString ("A");
+ return 0;
+}
+
+/* The tp_traverse callback for the A type. */
+static int
+A_traverse_impl (PyObject *self, visitproc visit, void *arg)
+{
+ auto *obj = (A *) self;
+ printf ("--> A_traverse_impl\n");
+ gdbpy_dict_attr_Py_VISIT (obj);
+ Py_VISIT (obj->m_a);
+ return 0;
+}
+
+static int
+A_traverse (PyObject *self, visitproc visit, void *arg)
+{
+ return gdbpy_heap_type::tp_traverse (self, visit, arg,
+ nullptr, A_traverse_impl);
+}
+
+/* The tp_clear callback for the A type. */
+
+static void
+A_clear_impl (PyObject *self)
+{
+ auto *obj = (A *) self;
+ printf ("--> A_clear_impl\n");
+ gdbpy_dict_attr_Py_CLEAR (obj);
+ Py_CLEAR (obj->m_a);
+}
+
+static void
+A_clear (PyObject *self)
+{
+ return gdbpy_heap_type::tp_clear (self, nullptr, A_clear_impl);
+}
+
+PyObject *
+A_name (PyObject *self, PyObject *args)
+{
+ auto *obj = (A *) self;
+ Py_INCREF (obj->m_a);
+ return obj->m_a;
+}
+
+PyObject *
+A_iama (PyObject *self, PyObject *args)
+{
+ auto *obj = (A *) self;
+ return PyUnicode_FromFormat ("Hello I am %U", obj->m_a);
+}
+
+gdb_PyGetSetDef A_object_getset[] =
+{
+ gdbpy_dict_attr_getter (A, "A"),
+ { nullptr } /* Sentinel */
+};
+
+PyMethodDef A_object_methods [] = {
+ { "name", A_name, METH_NOARGS,
+ "name () -> String.\n\
+Return the name of the class as a string value." },
+ { "iama", A_iama, METH_NOARGS,
+ "iama () -> String.\n\
+Return the object's greeting as a string value." },
+ {nullptr} /* Sentinel */
+};
+
+PyType_Slot A_object_type_slot[] = {
+ {Py_tp_doc, const_cast<char *>("A object")},
+ gdbpy_heap_type_methods (A_traverse, A_clear),
+ gdbpy_dict_attr_PyType_Slots (A),
+ {Py_tp_init, (void*) A_init},
+ {Py_tp_methods, A_object_methods},
+ {Py_tp_getset, A_object_getset},
+ {0, nullptr}, /* Sentinel */
+};
+
+PyType_Spec A_object_type_spec = {
+ "MyModule.A", /* tp_name */
+ sizeof (A), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ gdbpy_heap_type::tp_flags_default ()
+ | Py_TPFLAGS_BASETYPE, /* tp_flags */
+ A_object_type_slot,
+};
+
+} // namespace anonymous
+
+gdbpy_object_type_ref
+A::record_class (gdbpy_borrowed_ref<> module,
+ gdbpy_object_type_bref base_class ATTRIBUTE_UNUSED)
+{
+ gdbpy_object_type_ref obj_type
+ = gdbpy_type_initialize (module,
+ &A_object_type_spec);
+ return obj_type;
+}
diff --git a/src/B.cpp b/src/B.cpp
new file mode 100644
index 0000000..4dfd3a9
--- /dev/null
+++ b/src/B.cpp
@@ -0,0 +1,116 @@
+#include "my_py_module.hpp"
+
+namespace {
+ extern PyType_Spec B_object_type_spec;
+ gdbpy_object_type_bref B_base_type = nullptr;
+} /* namespace anonymous */
+
+namespace {
+
+static int
+B_init(PyObject *self, PyObject *args, PyObject *kwds)
+{
+ auto* obj = (B *) self;
+
+ /* super().__init__(*args, **kwargs) */
+ initproc base_func_init = (initproc) B_base_type.slot (Py_tp_init);
+ gdb_assert (base_func_init != nullptr);
+ if (base_func_init (self, args, kwds) != 0)
+ return -1;
+
+ obj->m_b = PyUnicode_FromString ("B");
+ return 0;
+}
+
+/* The tp_traverse callback for the A type. */
+static int
+B_traverse_impl (PyObject *self, visitproc visit, void *arg)
+{
+ auto *obj = (B *) self;
+ printf ("--> B_traverse_impl\n");
+ Py_VISIT (obj->m_b);
+ return 0;
+}
+
+static int
+B_traverse(PyObject *self, visitproc visit, void *arg)
+{
+ return gdbpy_heap_type::tp_traverse(self, visit, arg,
+ B_base_type, B_traverse_impl);
+}
+
+/* The tp_clear callback for the A type. */
+
+static void
+B_clear_impl (PyObject *self)
+{
+ auto *obj = (B *) self;
+ printf ("--> B_clear_impl\n");
+ Py_CLEAR (obj->m_b);
+}
+
+static void
+B_clear (PyObject *self)
+{
+ gdbpy_heap_type::tp_clear(self, B_base_type, B_clear_impl);
+}
+
+PyObject *
+B_name (PyObject *self, PyObject *args)
+{
+ auto *obj = (B *) self;
+ gdb_assert (obj->m_b != nullptr);
+ Py_INCREF (obj->m_b);
+ return obj->m_b;
+}
+
+PyObject *
+B_iamb (PyObject *self, PyObject *args)
+{
+ auto *obj = (B *) self;
+ gdb_assert (obj->m_a != nullptr);
+ gdb_assert (obj->m_b != nullptr);
+ return PyUnicode_FromFormat ("Hello I am both %U and %U", obj->m_a, obj->m_b);
+}
+
+PyMethodDef B_object_methods [] = {
+ { "name", B_name, METH_NOARGS,
+ "name () -> String.\n\
+Return the name of the class as a string value." },
+ { "iamb", B_iamb, METH_NOARGS,
+ "iamb () -> String.\n\
+Return the object's greeting as a string value." },
+ {nullptr} /* Sentinel */
+};
+
+PyType_Slot B_object_type_slot[] = {
+ {Py_tp_doc, const_cast<char *>("B object")},
+ gdbpy_heap_type_methods (B_traverse, B_clear),
+ {Py_tp_init, (void*) B_init},
+ {Py_tp_methods, B_object_methods},
+ {0, nullptr}, /* Sentinel */
+};
+
+PyType_Spec B_object_type_spec = {
+ "MyModule.B", /* tp_name */
+ sizeof (B), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ gdbpy_heap_type::tp_flags_default ()
+ | Py_TPFLAGS_BASETYPE, /* tp_flags */
+ B_object_type_slot,
+};
+
+} // namespace anonymous
+
+gdbpy_object_type_ref
+B::record_class (gdbpy_borrowed_ref<> module,
+ gdbpy_object_type_bref A_base_class)
+{
+ B_base_type = A_base_class;
+
+ gdbpy_object_type_ref obj_type
+ = gdbpy_type_initialize (module,
+ &B_object_type_spec,
+ A_base_class.get ());
+ return obj_type;
+}
diff --git a/src/ObjWithAlloc.cpp b/src/ObjWithAlloc.cpp
new file mode 100644
index 0000000..ed0b377
--- /dev/null
+++ b/src/ObjWithAlloc.cpp
@@ -0,0 +1,140 @@
+#include "my_py_module.hpp"
+
+namespace {
+ extern PyType_Spec ObjWithAlloc_object_type_spec;
+ gdbpy_object_type_bref ObjWithAlloc_object_type;
+} /* namespace anonymous */
+
+namespace {
+
+static PyObject *
+ObjWithAlloc_alloc ()
+{
+ gdbpy_heap_type_allocate_or_bailout
+ (my_new_obj, ObjWithAlloc, ObjWithAlloc_object_type);
+
+ my_new_obj->msg = new std::string (
+ "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, "
+ "consectetur, adipisci velit...");
+
+ my_new_obj->m_name = PyUnicode_FromString ("ObjWithAlloc");
+
+ my_new_obj->self_cycle = my_new_obj.strong_ref ().release ();
+
+ /* This is only an example to illustrate the usage of bailout(). */
+ if (*my_new_obj->msg == "hello world !")
+ {
+ delete my_new_obj->msg;
+ Py_XDECREF (my_new_obj->m_name);
+ Py_XDECREF (my_new_obj->self_cycle);
+ return my_new_obj.bailout ();
+ }
+ return my_new_obj.track ().release ();
+}
+
+/* The tp_traverse callback for the A type. */
+static int
+ObjWithAlloc_traverse_impl (PyObject *self, visitproc visit, void *arg)
+{
+ auto *obj = (ObjWithAlloc *) self;
+ printf ("--> ObjWithAlloc_traverse_impl\n");
+ Py_VISIT (obj->m_name);
+ Py_VISIT (obj->self_cycle);
+ return 0;
+}
+
+static int
+ObjWithAlloc_traverse (PyObject *self, visitproc visit, void *arg)
+{
+ return gdbpy_heap_type::tp_traverse (self, visit, arg,
+ nullptr, ObjWithAlloc_traverse_impl);
+}
+
+/* The tp_clear callback for the A type. */
+
+static void
+ObjWithAlloc_clear_impl (PyObject *self)
+{
+ auto *obj = (ObjWithAlloc *) self;
+ printf ("--> ObjWithAlloc_clear_impl\n");
+ Py_CLEAR (obj->m_name);
+ Py_CLEAR (obj->self_cycle);
+ if (obj->msg != nullptr)
+ {
+ delete obj->msg;
+ obj->msg = nullptr;
+ }
+}
+
+static void
+ObjWithAlloc_clear (PyObject *self)
+{
+ return gdbpy_heap_type::tp_clear (self, nullptr, ObjWithAlloc_clear_impl);
+}
+
+PyObject *
+ObjWithAlloc_name (PyObject *self, PyObject *args)
+{
+ auto *obj = (ObjWithAlloc *) self;
+ Py_INCREF (obj->m_name);
+ return obj->m_name;
+}
+
+PyObject *
+ObjWithAlloc_whoami (PyObject *self, PyObject *args)
+{
+ auto *obj = (ObjWithAlloc *) self;
+ return PyUnicode_FromFormat ("Hello I am %U", obj->m_name);
+}
+
+PyObject *
+ObjWithAlloc_speak (PyObject *self, PyObject *args)
+{
+ auto *obj = (ObjWithAlloc *) self;
+ return PyUnicode_FromFormat ("I, %U, %s", obj->m_name, obj->msg->c_str ());
+}
+
+PyMethodDef ObjWithAlloc_object_methods [] = {
+ { "name", ObjWithAlloc_name, METH_NOARGS,
+ "name () -> String.\n\
+Return the name of the class as a string value." },
+ { "whoami", ObjWithAlloc_whoami, METH_NOARGS,
+ "whoami () -> String.\n\
+Return the object's greeting as a string value." },
+ { "speak", ObjWithAlloc_speak, METH_NOARGS,
+ "speak () -> String.\n\
+Return the object's speech as a string value." },
+ {nullptr} /* Sentinel */
+};
+
+PyType_Slot ObjWithAlloc_object_type_slot[] = {
+ {Py_tp_doc, const_cast<char *>("ObjWithAlloc object")},
+ gdbpy_heap_type_methods (ObjWithAlloc_traverse, ObjWithAlloc_clear),
+ {Py_tp_alloc, (void*) ObjWithAlloc_alloc},
+ {Py_tp_methods, ObjWithAlloc_object_methods},
+ {0, nullptr}, /* Sentinel */
+};
+
+PyType_Spec ObjWithAlloc_object_type_spec = {
+ "MyModule.ObjWithAlloc", /* tp_name */
+ sizeof (ObjWithAlloc), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ gdbpy_heap_type::tp_flags_default ()
+ | Py_TPFLAGS_BASETYPE, /* tp_flags */
+ ObjWithAlloc_object_type_slot,
+};
+
+} // namespace anonymous
+
+gdbpy_object_type_ref
+ObjWithAlloc::record_class
+ (gdbpy_borrowed_ref<> module,
+ gdbpy_object_type_bref base_class ATTRIBUTE_UNUSED)
+{
+ gdbpy_object_type_ref obj_type
+ = gdbpy_type_initialize (module,
+ &ObjWithAlloc_object_type_spec);
+
+ ObjWithAlloc_object_type = obj_type;
+ return obj_type;
+}
diff --git a/src/gdb-others.cpp b/src/gdb-others.cpp
new file mode 100644
index 0000000..10e51d1
--- /dev/null
+++ b/src/gdb-others.cpp
@@ -0,0 +1,31 @@
+#include "gdb-others.hpp"
+
+#define TOOLNAME "GDB"
+
+#include <cstdarg>
+#include <cstdlib>
+#include <cstdio>
+
+extern "C" {
+
+[[noreturn]] static void
+internal_verror (const char *file, int line, const char *fmt, va_list args)
+{
+ fprintf (stderr, "\
+%s:%d: A problem internal to " TOOLNAME " has been detected.\n", file, line);
+ vfprintf (stderr, fmt, args);
+ fprintf (stderr, "\n");
+ abort ();
+}
+
+void
+internal_error_loc (const char *file, int line, const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start (ap, fmt);
+ internal_verror (file, line, fmt, ap);
+ va_end (ap);
+}
+
+}
diff --git a/src/my_py_module.cpp b/src/my_py_module.cpp
new file mode 100644
index 0000000..edb62cc
--- /dev/null
+++ b/src/my_py_module.cpp
@@ -0,0 +1,29 @@
+#include <Python.h>
+
+#include "my_py_module.hpp"
+
+namespace {
+
+// A struct contains the definition of a module
+PyModuleDef my_module = {
+ PyModuleDef_HEAD_INIT,
+ "MyModule",
+ "This is MyModule's docstring",
+ -1, // Optional size of the module state memory
+ NULL, // Optional module methods
+ NULL, // Optional slot definitions
+ NULL, // Optional traversal function
+ NULL, // Optional clear function
+ NULL // Optional module deallocation function
+};
+
+} // namespace anonymous
+
+PyMODINIT_FUNC
+PyInit_MyModule(void) {
+ gdbpy_borrowed_ref<> module = PyModule_Create(&my_module);
+ auto A_object_type = A::record_class (module);
+ auto B_object_type = B::record_class (module, A_object_type);
+ auto ObjWithAlloc_object_type = ObjWithAlloc::record_class (module);
+ return module;
+}
diff --git a/src/my_py_module.hpp b/src/my_py_module.hpp
new file mode 100644
index 0000000..6fc173f
--- /dev/null
+++ b/src/my_py_module.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "python-internal.hpp"
+
+#include <string>
+#include <string_view>
+
+struct A: public PyObject
+{
+ /* Dictionary holding user-added attributes.
+ This is the __dict__ attribute of the object. */
+ GDBPYOBJECT_DICT_ATTR (A);
+
+ PyObject *m_a;
+
+ static gdbpy_object_type_ref
+ record_class (gdbpy_borrowed_ref<> module,
+ gdbpy_object_type_bref base_class = nullptr);
+};
+
+static_assert (gdb::is_python_allocatable_v<A>);
+
+struct B: public A
+{
+ PyObject *m_b;
+
+ static gdbpy_object_type_ref
+ record_class (gdbpy_borrowed_ref<> module,
+ gdbpy_object_type_bref base_class = nullptr);
+};
+
+static_assert (gdb::is_python_allocatable_v<B>);
+
+struct ObjWithAlloc: public PyObject
+{
+ const std::string *msg;
+ PyObject *m_name;
+ PyObject *self_cycle;
+
+ static gdbpy_object_type_ref
+ record_class (gdbpy_borrowed_ref<> module,
+ gdbpy_object_type_bref base_class = nullptr);
+};
+
+static_assert (gdb::is_python_allocatable_v<ObjWithAlloc>);
diff --git a/src/py-obj-type.cpp b/src/py-obj-type.cpp
new file mode 100644
index 0000000..6add108
--- /dev/null
+++ b/src/py-obj-type.cpp
@@ -0,0 +1,80 @@
+/* Helpers related to Python object type
+
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include "python-internal.hpp"
+#include "py-obj-type.hpp"
+
+/* Return the type's fully qualified name from a PyTypeObject. */
+std::string
+gdb_py_tp_name (PyTypeObject *py_type) noexcept
+{
+ auto pyobj_to_str = [](PyObject *name) -> std::string
+ {
+ const char *s = PyUnicode_AsUTF8AndSize (name, nullptr);
+ return (s == nullptr) ? "" : s;
+ };
+
+#if PY_VERSION_HEX >= 0x030d0000
+ /* Note: PyType_GetFullyQualifiedName() was added in version 3.13, and is
+ part of the stable ABI since version 3.13. */
+ gdbpy_ref<> fully_qualified_name (PyType_GetFullyQualifiedName (py_type));
+ if (fully_qualified_name == nullptr)
+ return {};
+
+ return pyobj_to_str (fully_qualified_name.get ());
+
+#else /* PY_VERSION_HEX < 0x030d0000 && ! defined (Py_LIMITED_API) */
+ /* For non-heap types, the fully qualified name corresponds to tp_name. */
+ if (! (PyType_GetFlags (py_type) & Py_TPFLAGS_HEAPTYPE))
+ return py_type->tp_name;
+
+ /* In the absence of PyType_GetFullyQualifiedName(), we fallback using
+ __qualname__ instead. However, the result may differ slightly in some
+ cases, e.g. the module name may be missing. */
+
+# if PY_VERSION_HEX >= 0x030b0000
+ /* Note: PyType_GetQualName() was added in version 3.11. */
+ gdbpy_ref<> qualname (PyType_GetQualName (py_type));
+ if (qualname == nullptr)
+ return {};
+
+ return pyobj_to_str (qualname.get ());
+
+# else
+ /* In the absence of PyType_GetQualName(), fallback on using PyHeapTypeObject
+ which is not part of the public API.
+ Tested on 3.10 which is the oldest supported version at the time of this
+ writing, i.e. February 2026. Hopefully, this workaround should go away
+ when the minimum supported Python version is increased above 3.10. */
+ PyHeapTypeObject *ht = (PyHeapTypeObject *) py_type;
+ if (ht->ht_qualname == nullptr)
+ return {};
+
+ return pyobj_to_str (ht->ht_qualname);
+# endif
+#endif
+}
+
+/* Return the type's fully qualified name from a PyObject. */
+std::string
+gdbpy_py_obj_tp_name (PyObject *self) noexcept
+{
+ /* Note: Py_TYPE () is part of the stable ABI since version 3.14. */
+ return gdb_py_tp_name (Py_TYPE (self));
+}
diff --git a/src/python-internal.cpp b/src/python-internal.cpp
new file mode 100644
index 0000000..6b1a262
--- /dev/null
+++ b/src/python-internal.cpp
@@ -0,0 +1,87 @@
+#include "python-internal.hpp"
+#include <cstring>
+
+namespace {
+
+/* Like PyModule_AddObject, but does not steal a reference to
+ OBJECT. */
+
+bool
+gdb_pymodule_addobject (PyObject *mod, const char *name, PyObject *object)
+{
+ Py_INCREF (object);
+ int result = PyModule_AddObject (mod, name, object);
+ if (result < 0)
+ Py_DECREF (object);
+ return result == 0;
+}
+
+} // namespace anonymous
+
+/* A wrapper for all the initialization logic of the Python type.
+ - PyType_FromSpec or PyType_FromSpecWithBases initializes the opaque type
+ PyTypeObject (which is itself a PyObject) from the provided PyType_Spec.
+ - gdb_pymodule_addobject registers the type in the appropriate module.
+ If MOD is supplied, then the type is added to that module. If MOD
+ is not supplied, the type name (PyType_Spec.name field) must be of
+ the form "gdb.Mumble", and the type will be added to the gdb module.
+ Returns true on success, false on error. */
+
+gdbpy_object_type_ref
+gdbpy_type_initialize (gdbpy_borrowed_ref<> &mod,
+ PyType_Spec *spec,
+ PyObject *bases,
+ bool disallow_instantiation)
+{
+ gdbpy_ref<> res (bases == nullptr
+ ? PyType_FromSpec (spec)
+ : PyType_FromSpecWithBases (spec, bases));
+ if (res == nullptr)
+ return nullptr;
+
+ gdbpy_object_type_ref object_type (std::move (res));
+
+ if (object_type.same_as (nullptr))
+ {
+ PyErr_Print ();
+ return nullptr;
+ }
+
+#if PY_VERSION_HEX < 0x030a0000
+ /* When using the Python unlimited API, disallowing the creation of instances
+ of the type in Python is done by setting tp_free to NULL.
+ When using the Python limited API, PyType_FromSpec and its siblings
+ automatically set tp_new to PyType_GenericNew(), which has for consequence
+ of allowing the creation of instances of the type in Python.
+ To disallow this behavior with the limited API, a new flag was introduced
+ in Python 3.9: Py_TPFLAGS_DISALLOW_INSTANTIATION.
+ For Python versions older than 3.9, we need to set explicitly tp_free to
+ NULL once the type object was created. This workaround is only available
+ when the limited API is disabled. */
+#if defined (Py_LIMITED_API)
+ #error "The version of Python you are using does not expose " \
+ "Py_TPFLAGS_DISALLOW_INSTANTIATION as a part of the limited C API."
+#else
+ if (disallow_instantiation)
+ object_type.pytype ()->tp_new = nullptr;
+
+ /* PyType_Ready() is already called at the end of PyType_FromMetaclass()
+ (called by both PyType_FromSpec and PyType_FromSpecWithBases), so no
+ need to call it again unless the Python version does not have the flag
+ Py_TPFLAGS_DISALLOW_INSTANTIATION. In this case, a second call after
+ setting tp_new to NULL will disallow the instantiation for the type as
+ it used to be the case with non-heap types. */
+ if (PyType_Ready (object_type) < 0)
+ return nullptr;
+#endif
+#else
+ (void) disallow_instantiation;
+#endif
+
+ const char *dot = strrchr (spec->name, '.');
+ gdb_assert (dot != nullptr);
+ if (gdb_pymodule_addobject (mod.get (), dot + 1, object_type.get ()) < 0)
+ return nullptr;
+
+ return object_type;
+}
+
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: Prototype of C extension using Python limited API
2026-05-28 16:25 Prototype of C extension using Python limited API Matthieu Longo via Gdb
@ 2026-05-28 16:30 ` Matthieu Longo via Gdb
0 siblings, 0 replies; 2+ messages in thread
From: Matthieu Longo via Gdb @ 2026-05-28 16:30 UTC (permalink / raw)
To: Tom Tromey, gdb; +Cc: Andrew Burgess
On 28/05/2026 17:25, Matthieu Longo via Gdb wrote:
> Hi Tom,
>
> As I previously told you (at least, I believe so but I am not 100% sure), I finished migrating all
> the Python C extensions some time ago. I had discovered some issues with a case of inheritance
> causing unexpected crashes.
> To facilitate the debugging, I decided to implement a prototype outside of GDB, and managed to make
> it work. I finally got the prototype reviewed by Victor Stinner, a Python core developer, to double-
> check whether my design was working as expected.
>
> Please find attached my last iteration. I also provided an archive which should be more convenient
> for you if you want to compile and test it.
>
> Could you please review it so that we validate this new approach before I migrate all my previous
> patches to it ?
>
> PS:
> - a README is provided with all the needed instruction inside.
> - this patch contains code from outside GDB's repository, and should not be merged inside GDB's
> repository.
>
> Regards,
> Matthieu
Please ignore this email.
This is a duplicate of https://inbox.sourceware.org/gdb/21479c1d-3a9a-4ae2-bc1a-5dc19871f275@arm.com/
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-05-28 16:32 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-28 16:25 Prototype of C extension using Python limited API Matthieu Longo via Gdb
2026-05-28 16:30 ` Matthieu Longo via Gdb
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox