Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
* Re: [PATCH v2 4/6] gdb: new setters and getters for __dict__, and attributes
@ 2026-01-28 12:02 Matthieu Longo
  2026-01-28 18:00 ` Tom Tromey
  0 siblings, 1 reply; 9+ messages in thread
From: Matthieu Longo @ 2026-01-28 12:02 UTC (permalink / raw)
  To: gdb-patches, Tom Tromey; +Cc: Matthieu Longo

GDB is currently using the Python unlimited API. Migrating the codebase
to the Python limited API would have for benefit to make a GDB build
artifacts compatible with older and newer versions of Python that they
were built with.

This patch prepares the ground for migrating the existing C extension
types from static types to heap-allocated ones, by removing the
dependency on tp_dictoffset, which is unavailable when using the limited
API.

One of the most common incompatibilities in the current static type
declarations is the tp_dictoffset slot, which specifies the dictionary
offset within the instance structure. Historically, the unlimited
API has provided two approaches to supply a dictionary for __dict__:

 * A managed dictionary.
   Setting Py_TPFLAGS_MANAGED_DICT in tp_flags indicates that the
   instances of the type have a __dict__ attribute, and that the
   dictionary is managed by Python.
   According to the Python documentation, this is the recommended approach.
   However, this flag was introduced in 3.12, together with
   PyObject_VisitManagedDict() and PyObject_ClearManagedDict(), neither
   of which is part of the limited API (at least for now). As a result,
   this recommended approach is not viable in the context of the limited
   API.

 * An instance dictionary, for which the offset in the instance is
   provided via tp_dictoffset.
   According to the Python documentation, this "tp slot" is on the
   deprecation path, and Py_TPFLAGS_MANAGED_DICT should be used instead.
   Given the age of the GDB codebase and the requirement to support older
   Python versions (>= 3.4), no need to argue that today, the implementation
   of __dict__ relies on tp_dictoffset. However, in the context of the
   limited API, PyType_Slot does not provide a Py_tp_dictoffset member, so
   another approach is needed to provide __dict__ to instances of C extension
   types.

Given the constraints of the limited API, the proposed solution consists
in providing a dictionary through a common base class, gdbpy__dict__wrapper.
This helper class owns a dictionary member corresponding to __dict__, and
any C extension type requiring a __dict__ must inherit from it. Since
extension object must also be convertible to PyObject, this wrapper class
publicly inherits from PyObject as well.
Access to the dictionary is provided via a custom getter defined in a
PyGetSetDef, similarily to what was previously done with gdb_py_generic_dict().
Because __dict__ participates in attribute look-up, and since this dictionary
is neither managed by Python nor exposed via tp_dictoffset, custom
implementations of tp_getattro and tp_setattro are required to correctly
redirect attribute look-ups to the dictionary. These custom implementations
— equivalent to PyObject_GenericGetAttr() and PyObject_GenericSetAttr() —
must be installed via tp_getattro / tp_setattro for static types, or
Py_tp_getattro / Py_tp_setattro for heap-allocated types.

- gdbpy__dict__wrapper: a base class for C extension objects that own a
  __dict__.
- gdb_py_generic_dict_getter: a __dict__ getter for extension types
  derived from gdbpy__dict__wrapper.
- gdb_py_generic_getattro: equivalent of PyObject_GenericGetAttr, but
  fixes the look-up of __dict__.
- gdb_py_generic_setattro: equivalent of PyObject_GenericSetAttr, but
  fixes the look-up of __dict__.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=23830
---
 gdb/python/py-corefile.c     | 18 +++-----
 gdb/python/py-event.c        | 10 ++--
 gdb/python/py-event.h        |  8 +---
 gdb/python/py-inferior.c     | 29 ++----------
 gdb/python/py-infthread.c    | 10 ++--
 gdb/python/py-objfile.c      | 18 +++-----
 gdb/python/py-progspace.c    | 18 +++-----
 gdb/python/py-ref.h          | 38 +++++++++++++++
 gdb/python/py-type.c         | 19 +++-----
 gdb/python/py-utils.c        | 89 +++++++++++++++++++++++++++++++-----
 gdb/python/python-internal.h | 34 ++++++++++----
 11 files changed, 183 insertions(+), 108 deletions(-)

diff --git a/gdb/python/py-corefile.c b/gdb/python/py-corefile.c
index 6847722628f..24b573b2dbd 100644
--- a/gdb/python/py-corefile.c
+++ b/gdb/python/py-corefile.c
@@ -26,20 +26,14 @@
 
 /* A gdb.Corefile object.  */
 
-struct corefile_object
+struct corefile_object : public gdbpy_dict_wrapper
 {
-  PyObject_HEAD
-
   /* The inferior this core file is attached to.  This will be set to NULL
      when the inferior is deleted, or if a different core file is loaded
      for the inferior.  When this is NULL the gdb.Corefile object is
      considered invalid.*/
   struct inferior *inferior;
 
-  /* Dictionary holding user-added attributes.  This is the __dict__
-     attribute of the object.  This is an owning reference.  */
-  PyObject *dict;
-
   /* A Tuple of gdb.CorefileMappedFile objects.  This tuple is only created
      the first time the user calls gdb.Corefile.mapped_files(), the result
      is cached here.  If this pointer is not NULL then this is an owning
@@ -511,8 +505,8 @@ GDBPY_INITIALIZE_FILE (gdbpy_initialize_corefile);
 
 static gdb_PyGetSetDef corefile_getset[] =
 {
-  { "__dict__", gdb_py_generic_dict, nullptr,
-    "The __dict__ for the gdb.Corefile.", &corefile_object_type },
+  { "__dict__", gdb_py_generic_dict_getter, nullptr,
+    "The __dict__ for the gdb.Corefile.", nullptr },
   { "filename", cfpy_get_filename, nullptr,
     "The filename of a valid Corefile object.", nullptr },
   { nullptr }
@@ -548,8 +542,8 @@ PyTypeObject corefile_object_type =
   0,				  /*tp_hash */
   0,				  /*tp_call*/
   0,				  /*tp_str*/
-  0,				  /*tp_getattro*/
-  0,				  /*tp_setattro*/
+  gdb_py_generic_getattro,	  /*tp_getattro*/
+  gdb_py_generic_setattro,	  /*tp_setattro*/
   0,				  /*tp_as_buffer*/
   Py_TPFLAGS_DEFAULT,		  /*tp_flags*/
   "GDB corefile object",	  /* tp_doc */
@@ -566,7 +560,7 @@ PyTypeObject corefile_object_type =
   0,				  /* tp_dict */
   0,				  /* tp_descr_get */
   0,				  /* tp_descr_set */
-  offsetof (corefile_object, dict), /* tp_dictoffset */
+  0,				  /* tp_dictoffset */
   0,				  /* tp_init */
   0,				  /* tp_alloc */
   0,				  /* tp_new */
diff --git a/gdb/python/py-event.c b/gdb/python/py-event.c
index c985159a6f7..8fb21b7fdad 100644
--- a/gdb/python/py-event.c
+++ b/gdb/python/py-event.c
@@ -101,8 +101,8 @@ GDBPY_INITIALIZE_FILE (gdbpy_initialize_event);
 
 static gdb_PyGetSetDef event_object_getset[] =
 {
-  { "__dict__", gdb_py_generic_dict, NULL,
-    "The __dict__ for this event.", &event_object_type },
+  { "__dict__", gdb_py_generic_dict_getter, NULL,
+    "The __dict__ for this event.", NULL },
   { NULL }
 };
 
@@ -124,8 +124,8 @@ PyTypeObject event_object_type =
   0,                                          /* tp_hash  */
   0,                                          /* tp_call */
   0,                                          /* tp_str */
-  0,                                          /* tp_getattro */
-  0,                                          /* tp_setattro */
+  gdb_py_generic_getattro,                    /* tp_getattro */
+  gdb_py_generic_setattro,                    /* tp_setattro */
   0,                                          /* tp_as_buffer */
   Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* tp_flags */
   "GDB event object",                         /* tp_doc */
@@ -142,7 +142,7 @@ PyTypeObject event_object_type =
   0,                                          /* tp_dict */
   0,                                          /* tp_descr_get */
   0,                                          /* tp_descr_set */
-  offsetof (event_object, dict),              /* tp_dictoffset */
+  0,                                          /* tp_dictoffset */
   0,                                          /* tp_init */
   0                                           /* tp_alloc */
 };
diff --git a/gdb/python/py-event.h b/gdb/python/py-event.h
index 6c81d64eb4f..ec2e7bc03c5 100644
--- a/gdb/python/py-event.h
+++ b/gdb/python/py-event.h
@@ -31,12 +31,8 @@
 #include "py-event-types.def"
 #undef GDB_PY_DEFINE_EVENT_TYPE
 
-struct event_object
-{
-  PyObject_HEAD
-
-  PyObject *dict;
-};
+struct event_object : public gdbpy_dict_wrapper
+{};
 
 extern int emit_continue_event (ptid_t ptid);
 extern int emit_exited_event (const LONGEST *exit_code, struct inferior *inf);
diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c
index 8230f9d3943..69a7bfa3c37 100644
--- a/gdb/python/py-inferior.c
+++ b/gdb/python/py-inferior.c
@@ -32,25 +32,6 @@
 #include "progspace-and-thread.h"
 #include "gdbsupport/unordered_map.h"
 
-using thread_map_t
-  = gdb::unordered_map<thread_info *, gdbpy_ref<thread_object>>;
-
-struct inferior_object
-{
-  PyObject_HEAD
-
-  /* The inferior we represent.  */
-  struct inferior *inferior;
-
-  /* thread_object instances under this inferior.  This owns a
-     reference to each object it contains.  */
-  thread_map_t *threads;
-
-  /* Dictionary holding user-added attributes.
-     This is the __dict__ attribute of the object.  */
-  PyObject *dict;
-};
-
 extern PyTypeObject inferior_object_type;
 
 /* Deleter to clean up when an inferior is removed.  */
@@ -1061,8 +1042,8 @@ GDBPY_INITIALIZE_FILE (gdbpy_initialize_inferior);
 
 static gdb_PyGetSetDef inferior_object_getset[] =
 {
-  { "__dict__", gdb_py_generic_dict, nullptr,
-    "The __dict__ for this inferior.", &inferior_object_type },
+  { "__dict__", gdb_py_generic_dict_getter, nullptr,
+    "The __dict__ for this inferior.", nullptr },
   { "arguments", infpy_get_args, infpy_set_args,
     "Arguments to this program.", nullptr },
   { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL },
@@ -1144,8 +1125,8 @@ PyTypeObject inferior_object_type =
   0,				  /* tp_hash  */
   0,				  /* tp_call */
   0,				  /* tp_str */
-  0,				  /* tp_getattro */
-  0,				  /* tp_setattro */
+  gdb_py_generic_getattro,	  /* tp_getattro */
+  gdb_py_generic_setattro,	  /* tp_setattro */
   0,				  /* tp_as_buffer */
   Py_TPFLAGS_DEFAULT,		  /* tp_flags */
   "GDB inferior object",	  /* tp_doc */
@@ -1162,7 +1143,7 @@ PyTypeObject inferior_object_type =
   0,				  /* tp_dict */
   0,				  /* tp_descr_get */
   0,				  /* tp_descr_set */
-  offsetof (inferior_object, dict), /* tp_dictoffset */
+  0,				  /* tp_dictoffset */
   0,				  /* tp_init */
   0				  /* tp_alloc */
 };
diff --git a/gdb/python/py-infthread.c b/gdb/python/py-infthread.c
index e5d3222f9ae..d75742360d4 100644
--- a/gdb/python/py-infthread.c
+++ b/gdb/python/py-infthread.c
@@ -415,8 +415,8 @@ GDBPY_INITIALIZE_FILE (gdbpy_initialize_thread);
 
 static gdb_PyGetSetDef thread_object_getset[] =
 {
-  { "__dict__", gdb_py_generic_dict, nullptr,
-    "The __dict__ for this thread.", &thread_object_type },
+  { "__dict__", gdb_py_generic_dict_getter, nullptr,
+    "The __dict__ for this thread.", nullptr },
   { "name", thpy_get_name, thpy_set_name,
     "The name of the thread, as set by the user or the OS.", NULL },
   { "details", thpy_get_details, NULL,
@@ -479,8 +479,8 @@ PyTypeObject thread_object_type =
   0,				  /*tp_hash */
   0,				  /*tp_call*/
   0,				  /*tp_str*/
-  0,				  /*tp_getattro*/
-  0,				  /*tp_setattro*/
+  gdb_py_generic_getattro,	  /*tp_getattro*/
+  gdb_py_generic_setattro,	  /*tp_setattro*/
   0,				  /*tp_as_buffer*/
   Py_TPFLAGS_DEFAULT,		  /*tp_flags*/
   "GDB thread object",		  /* tp_doc */
@@ -497,7 +497,7 @@ PyTypeObject thread_object_type =
   0,				  /* tp_dict */
   0,				  /* tp_descr_get */
   0,				  /* tp_descr_set */
-  offsetof (thread_object, dict), /* tp_dictoffset */
+  0,				  /* tp_dictoffset */
   0,				  /* tp_init */
   0				  /* tp_alloc */
 };
diff --git a/gdb/python/py-objfile.c b/gdb/python/py-objfile.c
index 8cf365a27dc..36acdb06a25 100644
--- a/gdb/python/py-objfile.c
+++ b/gdb/python/py-objfile.c
@@ -26,17 +26,11 @@
 #include "python.h"
 #include "inferior.h"
 
-struct objfile_object
+struct objfile_object : public gdbpy_dict_wrapper
 {
-  PyObject_HEAD
-
   /* The corresponding objfile.  */
   struct objfile *objfile;
 
-  /* Dictionary holding user-added attributes.
-     This is the __dict__ attribute of the object.  */
-  PyObject *dict;
-
   /* The pretty-printer list of functions.  */
   PyObject *printers;
 
@@ -739,8 +733,8 @@ Look up a static-linkage global symbol in this objfile and return it." },
 
 static gdb_PyGetSetDef objfile_getset[] =
 {
-  { "__dict__", gdb_py_generic_dict, NULL,
-    "The __dict__ for this objfile.", &objfile_object_type },
+  { "__dict__", gdb_py_generic_dict_getter, NULL,
+    "The __dict__ for this objfile.", NULL },
   { "filename", objfpy_get_filename, NULL,
     "The objfile's filename, or None.", NULL },
   { "username", objfpy_get_username, NULL,
@@ -785,8 +779,8 @@ PyTypeObject objfile_object_type =
   0,				  /*tp_hash */
   0,				  /*tp_call*/
   0,				  /*tp_str*/
-  0,				  /*tp_getattro*/
-  0,				  /*tp_setattro*/
+  gdb_py_generic_getattro,	  /*tp_getattro*/
+  gdb_py_generic_setattro,	  /*tp_setattro*/
   0,				  /*tp_as_buffer*/
   Py_TPFLAGS_DEFAULT,		  /*tp_flags*/
   "GDB objfile object",		  /* tp_doc */
@@ -803,7 +797,7 @@ PyTypeObject objfile_object_type =
   0,				  /* tp_dict */
   0,				  /* tp_descr_get */
   0,				  /* tp_descr_set */
-  offsetof (objfile_object, dict), /* tp_dictoffset */
+  0,				  /* tp_dictoffset */
   0,				  /* tp_init */
   0,				  /* tp_alloc */
   objfpy_new,			  /* tp_new */
diff --git a/gdb/python/py-progspace.c b/gdb/python/py-progspace.c
index ee26b761adb..19f5e533b0a 100644
--- a/gdb/python/py-progspace.c
+++ b/gdb/python/py-progspace.c
@@ -29,17 +29,11 @@
 #include "observable.h"
 #include "inferior.h"
 
-struct pspace_object
+struct pspace_object : public gdbpy_dict_wrapper
 {
-  PyObject_HEAD
-
   /* The corresponding pspace.  */
   struct program_space *pspace;
 
-  /* Dictionary holding user-added attributes.
-     This is the __dict__ attribute of the object.  */
-  PyObject *dict;
-
   /* The pretty-printer list of functions.  */
   PyObject *printers;
 
@@ -758,8 +752,8 @@ GDBPY_INITIALIZE_FILE (gdbpy_initialize_pspace);
 
 static gdb_PyGetSetDef pspace_getset[] =
 {
-  { "__dict__", gdb_py_generic_dict, NULL,
-    "The __dict__ for this progspace.", &pspace_object_type },
+  { "__dict__", gdb_py_generic_dict_getter, NULL,
+    "The __dict__ for this progspace.", NULL },
   { "filename", pspy_get_filename, NULL,
     "The filename of the progspace's main symbol file, or None.", nullptr },
   { "symbol_file", pspy_get_symbol_file, nullptr,
@@ -821,8 +815,8 @@ PyTypeObject pspace_object_type =
   0,				  /*tp_hash */
   0,				  /*tp_call*/
   0,				  /*tp_str*/
-  0,				  /*tp_getattro*/
-  0,				  /*tp_setattro*/
+  gdb_py_generic_getattro,	  /*tp_getattro*/
+  gdb_py_generic_setattro,	  /*tp_setattro*/
   0,				  /*tp_as_buffer*/
   Py_TPFLAGS_DEFAULT,		  /*tp_flags*/
   "GDB progspace object",	  /* tp_doc */
@@ -839,7 +833,7 @@ PyTypeObject pspace_object_type =
   0,				  /* tp_dict */
   0,				  /* tp_descr_get */
   0,				  /* tp_descr_set */
-  offsetof (pspace_object, dict), /* tp_dictoffset */
+  0,				  /* tp_dictoffset */
   0,				  /* tp_init */
   0,				  /* tp_alloc */
   0,				  /* tp_new */
diff --git a/gdb/python/py-ref.h b/gdb/python/py-ref.h
index b09d88dc30d..4d44ca90e21 100644
--- a/gdb/python/py-ref.h
+++ b/gdb/python/py-ref.h
@@ -42,4 +42,42 @@ struct gdbpy_ref_policy
 template<typename T = PyObject> using gdbpy_ref
   = gdb::ref_ptr<T, gdbpy_ref_policy<T>>;
 
+/* A wrapper class for Python extension objects that have a __dict__ attribute.
+
+   Any Python C object extension needing __dict__ should inherit from this
+   class. Given that the C extension object must also be convertible to
+   PyObject, this wrapper class publicly inherits from PyObject as well.
+
+   Access to the dict requires a custom getter defined via PyGetSetDef.
+     gdb_PyGetSetDef my_object_getset[] =
+     {
+       { "__dict__", gdb_py_generic_dict_getter, nullptr,
+         "The __dict__ for this object.", nullptr },
+       ...
+       { 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:
+     - gdb_py_generic_getattro (), assigned to tp_getattro for static types,
+       or Py_tp_getattro for heap-allocated types.
+     - gdb_py_generic_setattro (), assigned to tp_setattro for static types,
+       or Py_tp_setattro for heap-allocated types.  */
+struct gdbpy_dict_wrapper : public PyObject
+{
+  /* Dictionary holding user-added attributes.
+     This is the __dict__ attribute of the object.  */
+  PyObject *dict;
+
+  /* Compute the address of the __dict__ attribute for the given PyObject.  */
+  static PyObject **compute_addr (PyObject *self)
+  {
+    auto *wrapper = reinterpret_cast<gdbpy_dict_wrapper *> (self);
+    return &wrapper->dict;
+  }
+};
+
 #endif /* GDB_PYTHON_PY_REF_H */
diff --git a/gdb/python/py-type.c b/gdb/python/py-type.c
index f39cb0240c8..76f001b99da 100644
--- a/gdb/python/py-type.c
+++ b/gdb/python/py-type.c
@@ -36,13 +36,8 @@ struct type_object : public PyObject
 extern PyTypeObject type_object_type;
 
 /* A Field object.  */
-struct field_object
-{
-  PyObject_HEAD
-
-  /* Dictionary holding our attributes.  */
-  PyObject *dict;
-};
+struct field_object : public gdbpy_dict_wrapper
+{};
 
 extern PyTypeObject field_object_type;
 
@@ -1707,8 +1702,8 @@ PyTypeObject type_object_type =
 
 static gdb_PyGetSetDef field_object_getset[] =
 {
-  { "__dict__", gdb_py_generic_dict, NULL,
-    "The __dict__ for this field.", &field_object_type },
+  { "__dict__", gdb_py_generic_dict_getter, NULL,
+    "The __dict__ for this field.", NULL },
   { NULL }
 };
 
@@ -1730,8 +1725,8 @@ PyTypeObject field_object_type =
   0,				  /*tp_hash */
   0,				  /*tp_call*/
   0,				  /*tp_str*/
-  0,				  /*tp_getattro*/
-  0,				  /*tp_setattro*/
+  gdb_py_generic_getattro,	  /*tp_getattro*/
+  gdb_py_generic_setattro,	  /*tp_setattro*/
   0,				  /*tp_as_buffer*/
   Py_TPFLAGS_DEFAULT,		  /*tp_flags*/
   "GDB field object",		  /* tp_doc */
@@ -1748,7 +1743,7 @@ PyTypeObject field_object_type =
   0,				  /* tp_dict */
   0,				  /* tp_descr_get */
   0,				  /* tp_descr_set */
-  offsetof (field_object, dict),  /* tp_dictoffset */
+  0,				  /* tp_dictoffset */
   0,				  /* tp_init */
   0,				  /* tp_alloc */
   0,				  /* tp_new */
diff --git a/gdb/python/py-utils.c b/gdb/python/py-utils.c
index 131230f80b3..e8485386d8d 100644
--- a/gdb/python/py-utils.c
+++ b/gdb/python/py-utils.c
@@ -309,24 +309,91 @@ gdb_py_int_as_long (PyObject *obj, long *result)
 
 \f
 
-/* Generic implementation of the __dict__ attribute for objects that
-   have a dictionary.  The CLOSURE argument should be the type object.
-   This only handles positive values for tp_dictoffset.  */
+/* Generic implementation of the getter for the __dict__ attribute for objects
+   having a dictionary.  The CLOSURE argument is unused.  */
 
 PyObject *
-gdb_py_generic_dict (PyObject *self, void *closure)
+gdb_py_generic_dict_getter (PyObject *self,
+			    void *closure ATTRIBUTE_UNUSED)
 {
-  PyObject *result;
-  PyTypeObject *type_obj = (PyTypeObject *) closure;
-  char *raw_ptr;
+  PyObject **py_dict_ptr = gdbpy_dict_wrapper::compute_addr (self);
+  PyObject *py_dict = *py_dict_ptr;
+  if (py_dict == nullptr)
+    {
+      PyErr_SetString (PyExc_AttributeError,
+		       "This object has no __dict__");
+      return nullptr;
+    }
+  return Py_NewRef (py_dict);
+}
 
-  raw_ptr = (char *) self + type_obj->tp_dictoffset;
-  result = * (PyObject **) raw_ptr;
+/* Generic attribute getter function similar to PyObject_GenericGetAttr () but
+   that should be used when the object has a dictionary __dict__.  */
+PyObject *
+gdb_py_generic_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 (gdb_py_generic_dict_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);
+
+  /* PyDict_GetItemWithError() has a misleading name that lets one believe that
+     the function sets PyExc_AttributeError when the key is not found, whereas
+     it actually does not and only returns NULL.
+     Not setting the exception for an unfound key causes the raising of another
+     exception:
+       <class 'SystemError'>: error return without exception set
+     Hence this surprising manual setting of an exception to makes everything
+     fit into place.  */
+  PyErr_Format (PyExc_AttributeError,
+		"'%s' object has no attribute '%s'",
+		Py_TYPE (self)->tp_name,
+		PyUnicode_AsUTF8AndSize (attr, nullptr));
+  return nullptr;
+}
 
-  Py_INCREF (result);
-  return result;
+/* Generic attribute setter function similar to PyObject_GenericSetAttr () but
+   that should be used when the object has a dictionary __dict__.  */
+int
+gdb_py_generic_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 (gdb_py_generic_dict_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);
 }
 
+\f
+
 /* Like PyModule_AddObject, but does not steal a reference to
    OBJECT.  */
 
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 95619bf775e..f6915a62b7a 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -107,6 +107,15 @@ typedef unsigned long gdb_py_ulongest;
 
 #endif /* HAVE_LONG_LONG */
 
+#if PY_VERSION_HEX < 0x030a0000
+static inline PyObject *
+Py_NewRef (PyObject *obj)
+{
+  Py_INCREF (obj);
+  return obj;
+}
+#endif
+
 /* A template variable holding the format character (as for
    Py_BuildValue) for a given type.  */
 template<typename T>
@@ -384,22 +393,27 @@ struct gdbpy_breakpoint_object
 extern gdbpy_breakpoint_object *bppy_pending_object;
 
 
-struct thread_object
+struct thread_object : public gdbpy_dict_wrapper
 {
-  PyObject_HEAD
-
   /* The thread we represent.  */
   struct thread_info *thread;
 
   /* The Inferior object to which this thread belongs.  */
   PyObject *inf_obj;
-
-  /* Dictionary holding user-added attributes.  This is the __dict__
-     attribute of the object.  */
-  PyObject *dict;
 };
 
-struct inferior_object;
+using thread_map_t
+  = gdb::unordered_map<thread_info *, gdbpy_ref<thread_object>>;
+
+struct inferior_object : public gdbpy_dict_wrapper
+{
+  /* The inferior we represent.  */
+  struct inferior *inferior;
+
+  /* thread_object instances under this inferior.  This owns a
+     reference to each object it contains.  */
+  thread_map_t *threads;
+};
 
 extern struct cmd_list_element *set_python_list;
 extern struct cmd_list_element *show_python_list;
@@ -989,7 +1003,9 @@ gdbpy_ref<> gdb_py_object_from_longest (LONGEST l);
 gdbpy_ref<> gdb_py_object_from_ulongest (ULONGEST l);
 int gdb_py_int_as_long (PyObject *, long *);
 
-PyObject *gdb_py_generic_dict (PyObject *self, void *closure);
+PyObject *gdb_py_generic_dict_getter (PyObject *self, void *closure);
+PyObject *gdb_py_generic_getattro (PyObject *self, PyObject *attr);
+int gdb_py_generic_setattro (PyObject *self, PyObject *attr, PyObject *value);
 
 int gdb_pymodule_addobject (PyObject *mod, const char *name,
 			    PyObject *object);
-- 
2.52.0


^ permalink raw reply	[flat|nested] 9+ messages in thread
* [PATCH v2 0/6] gdb: minor fixes for Python limited C API support
@ 2026-01-27 17:02 Matthieu Longo
  2026-01-27 17:02 ` [PATCH v2 4/6] gdb: new setters and getters for __dict__, and attributes Matthieu Longo
  0 siblings, 1 reply; 9+ messages in thread
From: Matthieu Longo @ 2026-01-27 17:02 UTC (permalink / raw)
  To: gdb-patches; +Cc: Tom Tromey, Matthieu Longo

This patch series fixes a set of minor issues encountered while enabling the Python limited C API in GDB, and starts preparing the ground for migrating the existing C extension types from static types to heap-allocated ones, by removing the dependency on tp_dictoffset, which is unavailable when using the limited API. Those fixes were accumulated during the migration work, and are intentionally split out from an upcoming larger patch series. The first three patches in the series are small, and self-contained fixes. The next three ones continue the work undertaken in the third one, and have a dependency on previous patches.

All changes were tested by building GDB against the limited API and unlimited API, and no regressions were observed in the testsuite.

# Revision 1 -> 2

Patch series v1: https://inbox.sourceware.org/gdb-patches/20260107165606.1719366-1-matthieu.longo@arm.com
Major changes since the last revision:
 - addressed minor comments on patch 1 and 2.
 - patch 3: inherits from PyObject.
New:
 - patch 4: inherits from PyObject, but also had to provide a different approach to provide a dict because offsetof does not work anymore with inheritance. Anyway, this had to be fixed so two birds with one stone.
 - patch 5: enforces that the template type is a subclass of PyObject in gdbpy_ref_policy.
 - patch 6: finish migrating all the remaing declarations.

Regards,
Matthieu


Matthieu Longo (6):
  Python limited API: migrate Py_CompileStringExFlags and PyRun_SimpleString
  Python limited API: migrate PyImport_ExtendInittab
  gdbpy_registry: cast C extension type object to PyObject * before Py_XINCREF
  gdb: new setters and getters for __dict__, and attributes
  gdb: cast all Python extension objects passed to gdbpy_ref_policy to PyObject*
  gdb: make remaining Python extension objects inherit from PyObject

 gdb/python/py-arch.c          |  4 +-
 gdb/python/py-block.c         |  8 +--
 gdb/python/py-breakpoint.c    |  4 +-
 gdb/python/py-cmd.c           |  4 +-
 gdb/python/py-color.c         |  4 +-
 gdb/python/py-connection.c    |  4 +-
 gdb/python/py-corefile.c      | 26 +++-------
 gdb/python/py-disasm.c        | 16 ++----
 gdb/python/py-event.c         | 10 ++--
 gdb/python/py-event.h         |  8 +--
 gdb/python/py-events.h        |  4 +-
 gdb/python/py-frame.c         |  4 +-
 gdb/python/py-gdb-readline.c  |  5 +-
 gdb/python/py-inferior.c      | 29 ++---------
 gdb/python/py-infthread.c     | 10 ++--
 gdb/python/py-instruction.c   |  5 +-
 gdb/python/py-lazy-string.c   |  5 +-
 gdb/python/py-linetable.c     | 12 ++---
 gdb/python/py-membuf.c        |  5 +-
 gdb/python/py-micmd.c         |  4 +-
 gdb/python/py-objfile.c       | 18 +++----
 gdb/python/py-param.c         |  4 +-
 gdb/python/py-prettyprint.c   |  6 +--
 gdb/python/py-progspace.c     | 18 +++----
 gdb/python/py-record-btrace.c |  5 +-
 gdb/python/py-record.c        |  4 +-
 gdb/python/py-record.h        |  8 +--
 gdb/python/py-ref.h           | 47 ++++++++++++++++-
 gdb/python/py-registers.c     | 20 +++-----
 gdb/python/py-style.c         |  4 +-
 gdb/python/py-symbol.c        |  4 +-
 gdb/python/py-symtab.c        |  8 +--
 gdb/python/py-tui.c           |  4 +-
 gdb/python/py-type.c          | 26 ++++------
 gdb/python/py-unwind.c        |  8 +--
 gdb/python/py-utils.c         | 95 +++++++++++++++++++++++++++++++----
 gdb/python/py-value.c         |  4 +-
 gdb/python/python-internal.h  | 47 ++++++++++++-----
 gdb/python/python.c           | 27 ++++------
 39 files changed, 281 insertions(+), 247 deletions(-)

-- 
2.52.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-01-29 14:48 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-01-28 12:02 [PATCH v2 4/6] gdb: new setters and getters for __dict__, and attributes Matthieu Longo
2026-01-28 18:00 ` Tom Tromey
2026-01-29 10:42   ` Matthieu Longo
2026-01-29 14:45     ` Tom Tromey
  -- strict thread matches above, loose matches on Subject: below --
2026-01-27 17:02 [PATCH v2 0/6] gdb: minor fixes for Python limited C API support Matthieu Longo
2026-01-27 17:02 ` [PATCH v2 4/6] gdb: new setters and getters for __dict__, and attributes Matthieu Longo
2026-01-27 19:06   ` Tom Tromey
2026-01-28 11:57     ` Matthieu Longo
2026-01-28 17:43     ` Matthieu Longo
2026-01-28 17:51       ` Tom Tromey

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox