From: Matthieu Longo via Gdb <gdb@sourceware.org>
To: Tom Tromey <tom@tromey.com>, gdb@sourceware.org
Cc: Andrew Burgess <aburgess@redhat.com>
Subject: Re: RFC: prototype of C extensions using the Python limited API
Date: Mon, 22 Jun 2026 11:24:52 +0100 [thread overview]
Message-ID: <143b06a7-117f-4500-a911-26f8f79eef2f@arm.com> (raw)
In-Reply-To: <21479c1d-3a9a-4ae2-bc1a-5dc19871f275@arm.com>
On 28/05/2026 17:24, 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
This might be easier to review.
Matthieu
From a5bb99c2df2ab85dc335f33ae5e06b624a4e7df2 Mon Sep 17 00:00:00 2001
From: Matthieu Longo <matthieu.longo@arm.com>
Date: Mon, 22 Jun 2026 10:36:17 +0100
Subject: [PATCH] first commit
---
CMakeLists.txt | 57 +++++
README.md | 65 ++++++
include/gdb-others.hpp | 64 ++++++
include/gdb_ref_ptr.hpp | 301 +++++++++++++++++++++++++
include/py-obj-dict.hpp | 189 ++++++++++++++++
include/py-obj-type.hpp | 424 ++++++++++++++++++++++++++++++++++++
include/py-ref.hpp | 156 +++++++++++++
include/python-internal.hpp | 14 ++
include/python-traits.hpp | 93 ++++++++
pydbg.cpp | 80 +++++++
src/A.cpp | 113 ++++++++++
src/B.cpp | 116 ++++++++++
src/ObjWithAlloc.cpp | 140 ++++++++++++
src/gdb-others.cpp | 31 +++
src/my_py_module.cpp | 29 +++
src/my_py_module.hpp | 45 ++++
src/py-obj-type.cpp | 80 +++++++
src/python-internal.cpp | 87 ++++++++
18 files changed, 2084 insertions(+)
create mode 100644 CMakeLists.txt
create mode 100644 README.md
create mode 100644 include/gdb-others.hpp
create mode 100644 include/gdb_ref_ptr.hpp
create mode 100644 include/py-obj-dict.hpp
create mode 100644 include/py-obj-type.hpp
create mode 100644 include/py-ref.hpp
create mode 100644 include/python-internal.hpp
create mode 100644 include/python-traits.hpp
create mode 100644 pydbg.cpp
create mode 100644 src/A.cpp
create mode 100644 src/B.cpp
create mode 100644 src/ObjWithAlloc.cpp
create mode 100644 src/gdb-others.cpp
create mode 100644 src/my_py_module.cpp
create mode 100644 src/my_py_module.hpp
create mode 100644 src/py-obj-type.cpp
create mode 100644 src/python-internal.cpp
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..f524e72
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,57 @@
+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)
+
+add_subdirectory(victor_stinner)
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..f254977
--- /dev/null
+++ b/include/py-ref.hpp
@@ -0,0 +1,156 @@
+/* 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);
+ }
+};
+
+/* 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;
+}
--
2.54.0
prev parent reply other threads:[~2026-06-22 10:27 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-28 16:24 Matthieu Longo via Gdb
2026-05-28 16:31 ` Matthieu Longo via Gdb
2026-06-01 10:10 ` Matthieu Longo via Gdb
2026-06-08 13:38 ` Matthieu Longo via Gdb
2026-06-10 16:10 ` Andrew Burgess via Gdb
2026-06-16 17:15 ` Matthieu Longo via Gdb
2026-06-18 20:46 ` Tom Tromey
2026-06-22 9:26 ` Matthieu Longo via Gdb
2026-06-29 10:37 ` Matthieu Longo via Gdb
2026-06-22 10:24 ` Matthieu Longo via Gdb [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=143b06a7-117f-4500-a911-26f8f79eef2f@arm.com \
--to=gdb@sourceware.org \
--cc=aburgess@redhat.com \
--cc=matthieu.longo@arm.com \
--cc=tom@tromey.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox