Mirror of the gdb mailing list
 help / color / mirror / Atom feed
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



      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