[RFC,v2,10/21] gdb/python: add gdb.Compunit

Message ID 20241121124714.419946-11-jan.vrany@labware.com
State New
Headers
Series Add Python "JIT" API |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-arm warning Skipped upon request
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 warning Skipped upon request

Commit Message

Jan Vrany Nov. 21, 2024, 12:47 p.m. UTC
  This commit introduces gdb.Compunit - a representation of struct
compunit_symtab in Python.

It also adds method gdb.Objfile.compunits() to get a list of compunits
for an objfile and adds compunit attribute to gdb.Block and gdb.Symtab
to access compunit containing given block or symbol table.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
---
 gdb/Makefile.in                          |   1 +
 gdb/NEWS                                 |   2 +
 gdb/doc/python.texi                      |  69 +++++
 gdb/python/py-block.c                    |  15 ++
 gdb/python/py-compunit.c                 | 319 +++++++++++++++++++++++
 gdb/python/py-objfile.c                  |  29 +++
 gdb/python/py-symtab.c                   |  14 +
 gdb/python/python-internal.h             |   3 +
 gdb/testsuite/gdb.python/py-block.exp    |   2 +
 gdb/testsuite/gdb.python/py-compunit.exp |  62 +++++
 gdb/testsuite/gdb.python/py-symtab.exp   |   8 +
 11 files changed, 524 insertions(+)
 create mode 100644 gdb/python/py-compunit.c
 create mode 100644 gdb/testsuite/gdb.python/py-compunit.exp
  

Comments

Eli Zaretskii Nov. 21, 2024, 1:39 p.m. UTC | #1
> From: Jan Vrany <jan.vrany@labware.com>
> CC: Jan Vrany <jan.vrany@labware.com>,
> 	Eli Zaretskii <eliz@gnu.org>
> Date: Thu, 21 Nov 2024 12:47:03 +0000
> 
> This commit introduces gdb.Compunit - a representation of struct
> compunit_symtab in Python.
> 
> It also adds method gdb.Objfile.compunits() to get a list of compunits
> for an objfile and adds compunit attribute to gdb.Block and gdb.Symtab
> to access compunit containing given block or symbol table.
> 
> Reviewed-By: Eli Zaretskii <eliz@gnu.org>
> ---
>  gdb/Makefile.in                          |   1 +
>  gdb/NEWS                                 |   2 +
>  gdb/doc/python.texi                      |  69 +++++
>  gdb/python/py-block.c                    |  15 ++
>  gdb/python/py-compunit.c                 | 319 +++++++++++++++++++++++
>  gdb/python/py-objfile.c                  |  29 +++
>  gdb/python/py-symtab.c                   |  14 +
>  gdb/python/python-internal.h             |   3 +
>  gdb/testsuite/gdb.python/py-block.exp    |   2 +
>  gdb/testsuite/gdb.python/py-compunit.exp |  62 +++++
>  gdb/testsuite/gdb.python/py-symtab.exp   |   8 +
>  11 files changed, 524 insertions(+)
>  create mode 100644 gdb/python/py-compunit.c
>  create mode 100644 gdb/testsuite/gdb.python/py-compunit.exp

OK for the documentation part, thanks.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
  
Andrew Burgess Jan. 13, 2025, 6:15 p.m. UTC | #2
Jan Vrany <jan.vrany@labware.com> writes:

> This commit introduces gdb.Compunit - a representation of struct
> compunit_symtab in Python.
>
> It also adds method gdb.Objfile.compunits() to get a list of compunits
> for an objfile and adds compunit attribute to gdb.Block and gdb.Symtab
> to access compunit containing given block or symbol table.
>
> Reviewed-By: Eli Zaretskii <eliz@gnu.org>
> ---
>  gdb/Makefile.in                          |   1 +
>  gdb/NEWS                                 |   2 +
>  gdb/doc/python.texi                      |  69 +++++
>  gdb/python/py-block.c                    |  15 ++
>  gdb/python/py-compunit.c                 | 319 +++++++++++++++++++++++
>  gdb/python/py-objfile.c                  |  29 +++
>  gdb/python/py-symtab.c                   |  14 +
>  gdb/python/python-internal.h             |   3 +
>  gdb/testsuite/gdb.python/py-block.exp    |   2 +
>  gdb/testsuite/gdb.python/py-compunit.exp |  62 +++++
>  gdb/testsuite/gdb.python/py-symtab.exp   |   8 +
>  11 files changed, 524 insertions(+)
>  create mode 100644 gdb/python/py-compunit.c
>  create mode 100644 gdb/testsuite/gdb.python/py-compunit.exp
>
> diff --git a/gdb/Makefile.in b/gdb/Makefile.in
> index ecb323d8f02..88566b1d41a 100644
> --- a/gdb/Makefile.in
> +++ b/gdb/Makefile.in
> @@ -438,6 +438,7 @@ SUBDIR_PYTHON_SRCS = \
>  	python/py-value.c \
>  	python/py-varobj.c \
>  	python/py-xmethods.c \
> +	python/py-compunit.c \
>  	python/python.c
>  
>  SUBDIR_PYTHON_OBS = $(patsubst %.c,%.o,$(SUBDIR_PYTHON_SRCS))
> diff --git a/gdb/NEWS b/gdb/NEWS
> index 3d208744103..7273b23f989 100644
> --- a/gdb/NEWS
> +++ b/gdb/NEWS
> @@ -94,6 +94,8 @@
>    ** Added gdb.Type.function.  Returns a new gdb.Type representing a function
>       returning that type.  Parameter types can be specified too.
>  
> +  ** Added class gdb.Compunit.
> +
>  * Debugger Adapter Protocol changes
>  
>    ** The "scopes" request will now return a scope holding global
> diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
> index 272b51d32c5..2065f001320 100644
> --- a/gdb/doc/python.texi
> +++ b/gdb/doc/python.texi
> @@ -220,6 +220,7 @@ optional arguments while skipping others.  Example:
>  * Blocks In Python::            Accessing blocks from Python.
>  * Symbols In Python::           Python representation of symbols.
>  * Symbol Tables In Python::     Python representation of symbol tables.
> +* Compunits In Python::         Python representation of compunits.
>  * Line Tables In Python::       Python representation of line tables.
>  * Breakpoints In Python::       Manipulating breakpoints using Python.
>  * Finish Breakpoints in Python:: Setting Breakpoints on function return
> @@ -5691,6 +5692,11 @@ Like @code{Objfile.lookup_global_symbol}, but searches for a global
>  symbol with static linkage named @var{name} in this objfile.
>  @end defun
>  
> +@defun Objfile.compunits ()
> +Return a sequence of all the compunits associated with this objfile.
> +@xref{Compunits In Python}.
> +@end defun
> +
>  @node Frames In Python
>  @subsubsection Accessing inferior stack frames from Python
>  
> @@ -6043,6 +6049,11 @@ have a superblock that is not the static block -- for instance this
>  happens for an inlined function.
>  @end defvar
>  
> +@defvar Block.compunit
> +The compunit containing this block.  @xref{Compunits In Python}.
> +This attribute is not writable.
> +@end defvar
> +
>  @defvar Block.superblock
>  The block containing this block.  If this parent block does not exist,
>  this attribute holds @code{None}.  This attribute is not writable.
> @@ -6471,6 +6482,11 @@ If no producer information is available then @code{None} is returned.
>  This attribute is not writable.
>  @end defvar
>  
> +@defvar Symtab.compunit
> +The compunit this symbol table belongs to.  @xref{Compunits In Python}.
> +This attribute is not writable.
> +@end defvar
> +
>  A @code{gdb.Symtab} object has the following methods:
>  
>  @defun Symtab.is_valid ()
> @@ -6500,6 +6516,59 @@ Return the line table associated with the symbol table.
>  @xref{Line Tables In Python}.
>  @end defun
>  
> +@node Compunits In Python
> +@subsubsection Compunits representation in Python
> +
> +@cindex compunits in python
> +@tindex gdb.Compunit
> +
> +Access to compunits maintained by @value{GDBN} on objfiles
> +is exposed to Python via @code{gdb.Compunit}.  Compunit for a symbol table can
> +be accessed via @code{compunit} property of @code{gdb.Symtab} object.
> +@xref{Symbol Tables In Python}.  Method @code{compunits} of
> +@code{gdb.Objfile} can be used to get a list of all compunits belonging to
> +that objfile.  @xref{Objfiles In Python}.

Remember that user of the Python API are not only GDB developers.  As
such, I think this paragraph lacks detail about what a 'compunit' is.


> +
> +A @code{gdb.Compunit} object has the following attributes:
> +
> +@defvar Compunit.objfile
> +The compunits' backing object file.  @xref{Objfiles In Python}.
> +This attribute is not writable.

I prefer descriptions like this to include the actual object type, so:

  The @code{gdb.Objfile} from which the compunit was extracted.
  @xref{Objfiles In Python}.  This attribute is not writable.

I think this feedback applies to many of the new methods/attributes
added in this commit.

> +@end defvar
> +
> +@defvar Compunit.producer
> +The name and possibly version number of the program that
> +compiled the code in the compunit.
> +The contents of this string is up to the compiler.
> +If no producer information is available then @code{None} is returned.
> +This attribute is not writable.
> +@end defvar
> +
> +@defvar Compunit.symtabs
> +The list of symbol tables associated with this compunit.
> +@xref{Symbol Tables In Python}.  This attribute is not writable.
> +@end defvar
> +
> +A @code{gdb.Compunit} object has the following methods:
> +
> +@defun Compunit.is_valid ()
> +Returns @code{True} if the @code{gdb.Compunit} object is valid,
> +@code{False} if not.  A @code{gdb.Compunit} object can become invalid if
> +the compunit it refers to does not exist in @value{GDBN} any
> +longer.  All other @code{gdb.Compunit} methods will throw an exception
> +if it is invalid at the time the method is called.

The class attributes will also throw an exception is accessed, but this
text only mentions the methods.

> +@end defun
> +
> +@defun Compunit.global_block ()
> +Return the global block of the underlying compunit.
> +@xref{Blocks In Python}.
> +@end defun
> +
> +@defun Compunit.static_block ()
> +Return the static block of the underlying compunit.
> +@xref{Blocks In Python}.
> +@end defun
> +
>  @node Line Tables In Python
>  @subsubsection Manipulating line tables using Python
>  
> diff --git a/gdb/python/py-block.c b/gdb/python/py-block.c
> index 1c5eab44b3a..a05a2796c52 100644
> --- a/gdb/python/py-block.c
> +++ b/gdb/python/py-block.c
> @@ -215,6 +215,19 @@ blpy_get_static_block (PyObject *self, void *closure)
>    return block_to_block_object (static_block, self_obj->objfile);
>  }
>  
> +/* Getter function for Block.compunit.  */
> +
> +static PyObject *
> +blpy_get_compunit (PyObject *self, void *closure)
> +{
> +  const struct block *block;
> +
> +  BLPY_REQUIRE_VALID (self, block);
> +
> +  return compunit_to_compunit_object (
> +	   block->global_block ()->compunit ()).release ();

Line wrap before the opening '(' please.

> +}
> +
>  /* Implementation of gdb.Block.is_global (self) -> Boolean.
>     Returns True if this block object is a global block.  */
>  
> @@ -553,6 +566,8 @@ static gdb_PyGetSetDef block_object_getset[] = {
>      "Block containing the global block.", NULL },
>    { "static_block", blpy_get_static_block, NULL,
>      "Block containing the static block.", NULL },
> +  { "compunit", blpy_get_compunit, nullptr,
> +    "Compunit containing this block.", nullptr },
>    { "is_static", blpy_is_static, NULL,
>      "Whether this block is a static block.", NULL },
>    { "is_global", blpy_is_global, NULL,
> diff --git a/gdb/python/py-compunit.c b/gdb/python/py-compunit.c
> new file mode 100644
> index 00000000000..829746cc92d
> --- /dev/null
> +++ b/gdb/python/py-compunit.c
> @@ -0,0 +1,319 @@
> +/* Python interface to compunits.
> +
> +   Copyright (C) 2008-2024 Free Software Foundation, Inc.

When you rework this patch, remember to update the end date throughout.

> +
> +   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 "charset.h"
> +#include "symtab.h"
> +#include "source.h"
> +#include "python-internal.h"
> +#include "objfiles.h"
> +#include "block.h"
> +
> +struct compunit_object {
> +  PyObject_HEAD
> +
> +  /* The GDB compunit structure.  */
> +  struct compunit_symtab *compunit;
> +
> +  /* A compunit object is associated with an objfile, so keep track with
> +     a doubly-linked list, rooted in the objfile.  This allows
> +     invalidation of the underlying struct compunit_symtab when the objfile is
> +     deleted.  */
> +  compunit_object *prev;
> +  compunit_object *next;
> +};
> +
> +/* This function is called when an objfile is about to be freed.
> +   Invalidate the compunit as further actions on the compunit
> +   would result in bad data.  All access to obj->compunit should be
> +   gated by CUPY_REQUIRE_VALID which will raise an exception on
> +   compunits.  */
> +struct cupy_deleter
> +{
> +  void operator() (compunit_object *obj)
> +  {
> +    while (obj)

should be: while (obj != nullptr)

> +      {
> +	compunit_object *next = obj->next;
> +
> +	obj->compunit = nullptr;
> +	obj->next = nullptr;
> +	obj->prev = nullptr;
> +	obj = next;
> +      }
> +  }
> +};
> +
> +extern PyTypeObject compunit_object_type
> +    CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("compunit_object");
> +static const registry<objfile>::key<compunit_object, cupy_deleter>
> +     cupy_objfile_data_key;
> +
> +/* Require a valid compunit.  All access to compunit_object->compunit
> +   should be gated by this call.  */
> +
> +#define CUPY_REQUIRE_VALID(compunit_obj, compunit)		 \
> +  do {							 \
> +    compunit = compunit_object_to_compunit (compunit_obj);	 \
> +    if (compunit == nullptr)					 \
> +      {							 \
> +	PyErr_SetString (PyExc_RuntimeError,		 \
> +			 _("Compunit object is invalid.")); \
> +	return nullptr;					 \
> +      }							 \
> +  } while (0)
> +
> +
> +/* Getter function for gdb.Compunit.objfile.  */
> +
> +static PyObject *
> +cupy_get_objfile (PyObject *self, void *closure)
> +{
> +  struct compunit_symtab *compunit = nullptr;
> +
> +  CUPY_REQUIRE_VALID (self, compunit);
> +
> +  return objfile_to_objfile_object (compunit->objfile ()).release ();
> +}
> +
> +/* Getter function for gdb.Compunit.producer.  */
> +
> +static PyObject *
> +cupy_get_producer (PyObject *self, void *closure)
> +{
> +  struct compunit_symtab *compunit = nullptr;
> +
> +  CUPY_REQUIRE_VALID (self, compunit);
> +  if (compunit->producer () != nullptr)
> +    {
> +      const char *producer = compunit->producer ();
> +
> +      return host_string_to_python_string (producer).release ();
> +    }
> +
> +  Py_RETURN_NONE;
> +}
> +
> +/* Implementation of gdb.Compunit.is_valid (self) -> Boolean.
> +   Returns True if this Symbol table still exists in GDB.  */
> +
> +static PyObject *
> +cupy_is_valid (PyObject *self, PyObject *args)
> +{
> +  struct compunit_symtab *compunit = nullptr;
> +
> +  compunit = compunit_object_to_compunit (self);
> +  if (compunit == nullptr)
> +    Py_RETURN_FALSE;
> +
> +  Py_RETURN_TRUE;
> +}
> +
> +/* Return the GLOBAL_BLOCK of the underlying compunit.  */
> +
> +static PyObject *
> +cupy_global_block (PyObject *self, PyObject *args)
> +{
> +  struct compunit_symtab *compunit = nullptr;
> +  const struct blockvector *blockvector;

This declaration can be moved down to where blockvector is assigned.
And in cupy_static_block below.

> +
> +  CUPY_REQUIRE_VALID (self, compunit);
> +
> +  blockvector = compunit->blockvector ();
> +  const struct block *block = blockvector->global_block ();
> +
> +  return block_to_block_object (block, compunit->objfile ());
> +}
> +
> +/* Return the STATIC_BLOCK of the underlying compunit.  */
> +
> +static PyObject *
> +cupy_static_block (PyObject *self, PyObject *args)
> +{
> +  struct compunit_symtab *compunit = nullptr;
> +  const struct blockvector *blockvector;
> +
> +  CUPY_REQUIRE_VALID (self, compunit);
> +
> +  blockvector = compunit->blockvector ();
> +  const struct block *block = blockvector->static_block ();
> +
> +  return block_to_block_object (block, compunit->objfile ());
> +}
> +
> +static PyObject *
> +cupy_get_symtabs (PyObject *self, void *closure)

Missing header comment.

> +{
> +  struct compunit_symtab *compunit = nullptr;
> +
> +  CUPY_REQUIRE_VALID (self, compunit);
> +
> +  gdbpy_ref<> list (PyList_New (0));
> +  if (list == nullptr)
> +    return nullptr;
> +
> +  for (struct symtab *each : compunit->filetabs ())
> +    {
> +      {

Double scope here.  Not sure why though.

> +	gdbpy_ref<> item (symtab_to_symtab_object (each));
> +	if (item.get () == nullptr
> +	      || PyList_Append (list.get (), item.get ()) == -1)
> +	    return nullptr;
> +	}
> +    }
> +
> +  return list.release ();
> +}
> +
> +static void
> +cupy_dealloc (PyObject *obj)

Missing header comment.

> +{
> +  compunit_object *compunit = (compunit_object *) obj;
> +
> +  if (compunit->prev)
> +    compunit->prev->next = compunit->next;
> +  else if (compunit->compunit)
> +    cupy_objfile_data_key.set (compunit->compunit->objfile (),
> +			       compunit->next);
> +  if (compunit->next)
> +    compunit->next->prev = compunit->prev;
> +  compunit->compunit = nullptr;
> +  Py_TYPE (obj)->tp_free (obj);
> +}

I ran out of time to test this code further


> +
> +/* Given a compunit, and a compunit_object that has previously been
> +   allocated and initialized, populate the compunit_object with the
> +   struct compunit_symtab data.  Also, register the compunit_object life-cycle
> +   with the life-cycle of the object file associated with this
> +   compunit, if needed.  */
> +static void
> +set_compunit (compunit_object *obj, struct compunit_symtab *compunit)
> +{
> +  obj->compunit = compunit;
> +  obj->prev = nullptr;
> +  if (compunit)
> +    {
> +      obj->next = cupy_objfile_data_key.get (compunit->objfile ());
> +      if (obj->next)
> +	obj->next->prev = obj;
> +      cupy_objfile_data_key.set (compunit->objfile (), obj);
> +    }
> +  else
> +    obj->next = nullptr;
> +}
> +
> +/* Return a new reference to gdb.Compunit Python object representing
> +   COMPUNIT.  Return NULL and set the Python error on failure.  */
> +gdbpy_ref<>
> +compunit_to_compunit_object (struct compunit_symtab *compunit)
> +{
> +  compunit_object *compunit_obj;
> +
> +  compunit_obj = PyObject_NEW(compunit_object, &compunit_object_type);

Missing space after PyObject_NEW.  And please use PyObject_New instead.
The macro is just an alias these days, and the rest of GDB uses the _New
function directly.

> +  if (compunit_obj)

if (compunit_obj != nullptr)

> +    set_compunit(compunit_obj, compunit);

Missing space before '('.

> +
> +  return gdbpy_ref<>::new_reference ((PyObject * )compunit_obj);
> +}

I don't really like the approach taken by this function.  Each time the
function is called we get a new gdb.Compunit object created and chained
onto the objfile.

Now, I know that the use of gdbpy_richcompare means that the different
objects will compare as equal.  But I think it would be better to just
reuse the existing objects, i.e. have this function search the chain of
objects looking for a match.

Additionally, and I didn't know if this was intentional or not,
PyObject_NEW returns a new reference, i.e. the object returned has a
refcount of 1 already.  So why you return the
gdbpy_ref<>::new_reference(), the object you return has a refcount of 2.

What this means is that cupy_dealloc is never called as each
compunit_object chained off the objfile has a base refcount of 1, and
when cupy_deleter::operator() is called, I think we end up leaking the
Python objects.

I think it's right that the objfile holds a reference to each of the
compunit_object objects, this ensures that, once created, the object
lives as long as the objfile does.

Then compunit_object_to_compunit should lookup existing objects off the
objfile and reuse them if found (or create a new one and add it to the
chain).

And finally cupy_deleter::operator() can decref on all the objects in
the chain when the objfile goes out of scope, this will allow
cupy_dealloc to be called (assuming the user holds no references).

I think with these changes, the need for a doubly linked list goes away,
and you can probably use a single linked list.

Thanks,
Andrew

> +
> +/* Return struct compunit_symtab reference that is wrapped by this object.  */
> +struct compunit_symtab *
> +compunit_object_to_compunit (PyObject *obj)
> +{
> +  if (! PyObject_TypeCheck (obj, &compunit_object_type))
> +    return nullptr;
> +  return ((compunit_object *) obj)->compunit;
> +}
> +
> +static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
> +gdbpy_initialize_compunits (void)
> +{
> +  if (gdbpy_type_ready (&compunit_object_type) < 0)
> +    return -1;
> +
> +  return 0;
> +}
> +
> +GDBPY_INITIALIZE_FILE (gdbpy_initialize_compunits);
> +
> +
> +
> +static gdb_PyGetSetDef compunit_object_getset[] = {
> +  { "objfile", cupy_get_objfile, nullptr, "The compunit's objfile.",
> +    nullptr },
> +  { "producer", cupy_get_producer, nullptr,
> +    "The name/version of the program that compiled this compunit.", nullptr },
> +  { "symtabs", cupy_get_symtabs, nullptr,
> +    "List of symbol tables associated with this compunit", nullptr },
> +  {nullptr}  /* Sentinel */
> +};
> +
> +static PyMethodDef compunit_object_methods[] = {
> +  { "is_valid", cupy_is_valid, METH_NOARGS,
> +    "is_valid () -> Boolean.\n\
> +Return true if this compunit is valid, false if not." },
> +  { "global_block", cupy_global_block, METH_NOARGS,
> +    "global_block () -> gdb.Block.\n\
> +Return the global block of the compunit." },
> +  { "static_block", cupy_static_block, METH_NOARGS,
> +    "static_block () -> gdb.Block.\n\
> +Return the static block of the compunit." },
> +  {nullptr}  /* Sentinel */
> +};
> +
> +PyTypeObject compunit_object_type = {
> +  PyVarObject_HEAD_INIT (nullptr, 0)
> +  "gdb.Compunit",		  /*tp_name*/
> +  sizeof (compunit_object),	  /*tp_basicsize*/
> +  0,				  /*tp_itemsize*/
> +  cupy_dealloc,			  /*tp_dealloc*/
> +  0,				  /*tp_print*/
> +  0,				  /*tp_getattr*/
> +  0,				  /*tp_setattr*/
> +  0,				  /*tp_compare*/
> +  0,				  /*tp_repr*/
> +  0,				  /*tp_as_number*/
> +  0,				  /*tp_as_sequence*/
> +  0,				  /*tp_as_mapping*/
> +  0,				  /*tp_hash */
> +  0,				  /*tp_call*/
> +  0,			          /*tp_str*/
> +  0,				  /*tp_getattro*/
> +  0,				  /*tp_setattro*/
> +  0,				  /*tp_as_buffer*/
> +  Py_TPFLAGS_DEFAULT,		  /*tp_flags*/
> +  "GDB compunit object",	  /*tp_doc */
> +  0,				  /*tp_traverse */
> +  0,				  /*tp_clear */
> +  gdbpy_richcompare<compunit_object, compunit_symtab,
> +      &compunit_object::compunit>,/*tp_richcompare */
> +  0,				  /*tp_weaklistoffset */
> +  0,				  /*tp_iter */
> +  0,				  /*tp_iternext */
> +  compunit_object_methods,	  /*tp_methods */
> +  0,				  /*tp_members */
> +  compunit_object_getset,	  /*tp_getset */
> +  0,				  /* tp_base */
> +  0,				  /* tp_dict */
> +  0,				  /* tp_descr_get */
> +  0,				  /* tp_descr_set */
> +  0,				  /* tp_dictoffset */
> +  0,				  /* tp_init */
> +  0,				  /* tp_alloc */
> +  PyType_GenericNew		  /* tp_new */
> +};
> diff --git a/gdb/python/py-objfile.c b/gdb/python/py-objfile.c
> index 6ce58a1f6ed..e006fcc6e75 100644
> --- a/gdb/python/py-objfile.c
> +++ b/gdb/python/py-objfile.c
> @@ -544,6 +544,31 @@ objfpy_repr (PyObject *self_)
>  			       objfile_name (obj));
>  }
>  
> +/* Implementation of gdb.Objfile.compunits() -> List  */
> +
> +static PyObject *
> +objfpy_compunits (PyObject *self_, PyObject *args)
> +{
> +  objfile_object *self = (objfile_object *) self_;
> +
> +  OBJFPY_REQUIRE_VALID (self);
> +
> +  gdbpy_ref<> list (PyList_New (0));
> +  if (list == nullptr)
> +    return nullptr;
> +
> +  for (struct compunit_symtab *compunit : self->objfile->compunits ())
> +    {
> +      gdbpy_ref<> item = compunit_to_compunit_object (compunit);
> +
> +      if (item.get () == nullptr
> +	    || PyList_Append (list.get (), item.get ()) == -1)
> +	return nullptr;
> +    }
> +
> +  return list.release ();
> +}
> +
>  /* Subroutine of gdbpy_lookup_objfile_by_build_id to simplify it.
>     Return non-zero if STRING is a potentially valid build id.  */
>  
> @@ -737,6 +762,10 @@ Look up a global symbol in this objfile and return it." },
>      "lookup_static_symbol (name [, domain]).\n\
>  Look up a static-linkage global symbol in this objfile and return it." },
>  
> +  { "compunits", objfpy_compunits, METH_NOARGS,
> +    "compunits () -> List.\n\
> +Return a sequence of compunits associated to this objfile." },
> +
>    { NULL }
>  };
>  
> diff --git a/gdb/python/py-symtab.c b/gdb/python/py-symtab.c
> index 5330b15ef8d..dcb78e0045f 100644
> --- a/gdb/python/py-symtab.c
> +++ b/gdb/python/py-symtab.c
> @@ -193,6 +193,18 @@ stpy_get_producer (PyObject *self, void *closure)
>    Py_RETURN_NONE;
>  }
>  
> +/* Getter function for Symtab.compunit.  */
> +
> +static PyObject *
> +stpy_get_compunit (PyObject *self, void *closure)
> +{
> +  struct symtab *symtab = nullptr;
> +
> +  STPY_REQUIRE_VALID (self, symtab);
> +
> +  return compunit_to_compunit_object (symtab->compunit ()).release ();
> +}
> +
>  static PyObject *
>  stpy_fullname (PyObject *self, PyObject *args)
>  {
> @@ -533,6 +545,8 @@ static gdb_PyGetSetDef symtab_object_getset[] = {
>      NULL },
>    { "producer", stpy_get_producer, NULL,
>      "The name/version of the program that compiled this symtab.", NULL },
> +  { "compunit", stpy_get_compunit, nullptr,
> +    "The compunit this symtab belongs to.", nullptr },
>    {NULL}  /* Sentinel */
>  };
>  
> diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
> index 4d4810dc4cc..0a52cb86b4a 100644
> --- a/gdb/python/python-internal.h
> +++ b/gdb/python/python-internal.h
> @@ -555,6 +555,8 @@ gdbpy_ref<thread_object> create_thread_object (struct thread_info *tp);
>  gdbpy_ref<> thread_to_thread_object (thread_info *thr);;
>  gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf);
>  
> +gdbpy_ref<> compunit_to_compunit_object (struct compunit_symtab *compunit);
> +
>  PyObject *gdbpy_buffer_to_membuf (gdb::unique_xmalloc_ptr<gdb_byte> buffer,
>  				  CORE_ADDR address, ULONGEST length);
>  
> @@ -571,6 +573,7 @@ struct symtab *symtab_object_to_symtab (PyObject *obj);
>  struct symtab_and_line *sal_object_to_symtab_and_line (PyObject *obj);
>  frame_info_ptr frame_object_to_frame_info (PyObject *frame_obj);
>  struct gdbarch *arch_object_to_gdbarch (PyObject *obj);
> +struct compunit_symtab *compunit_object_to_compunit (PyObject *obj);
>  
>  extern PyObject *gdbpy_execute_mi_command (PyObject *self, PyObject *args,
>  					   PyObject *kw);
> diff --git a/gdb/testsuite/gdb.python/py-block.exp b/gdb/testsuite/gdb.python/py-block.exp
> index 20f21711126..64ee12786c4 100644
> --- a/gdb/testsuite/gdb.python/py-block.exp
> +++ b/gdb/testsuite/gdb.python/py-block.exp
> @@ -104,6 +104,8 @@ gdb_test "python print (repr (block))" \
>      "Check block in many_locals_func"
>  gdb_test "python print (block.function)" "many_locals_func" \
>      "many_locals_func block"
> +gdb_test "python print(block.compunit)" "<gdb\.Compunit object at .*>" \
> +    "test compunit property"
>  
>  # Switch frames, then test for main block.
>  gdb_test "up" ".*"
> diff --git a/gdb/testsuite/gdb.python/py-compunit.exp b/gdb/testsuite/gdb.python/py-compunit.exp
> new file mode 100644
> index 00000000000..450cd6c9f1b
> --- /dev/null
> +++ b/gdb/testsuite/gdb.python/py-compunit.exp
> @@ -0,0 +1,62 @@
> +# Copyright (C) 2024-2024 Free Software Foundation, Inc.
> +
> +# 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/>.
> +
> +# This file is part of the GDB testsuite.  It tests the compunit
> +# support in Python.
> +
> +load_lib gdb-python.exp
> +
> +require allow_python_tests
> +
> +standard_testfile py-objfile.c
> +
> +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
> +    return -1
> +}
> +
> +if {![runto_main]} {
> +    return 0
> +}
> +
> +set python_error_text "Error occurred in Python.*"
> +
> +gdb_py_test_silent_cmd "python sym = gdb.lookup_symbol(\"some_var\")" \
> +    "Find a symbol in objfile" 1
> +gdb_py_test_silent_cmd "python objfile = sym\[0\].symtab.objfile" \
> +    "Get backing object file" 1
> +
> +gdb_test "python print (len(objfile.compunits()) > 0)" \
> +    "True" \
> +    "Get objfile compunits"
> +gdb_test "python print (objfile.compunits())" \
> +    "\\\[<gdb\.Compunit object at .*>\\\]" \
> +    "Objfile compunits return a sequence of gdb.Compunit"
> +gdb_test "python print (objfile.compunits()\[0\] == objfile.compunits()\[0\])" \
> +    "True" \
> +    "Compunits are comparable"
> +gdb_test "python print (len(objfile.compunits()\[0\].symtabs) > 0)" \
> +    "True" \
> +    "Get compunit symtabs"
> +gdb_test "python print (objfile.compunits()\[0\].symtabs)" \
> +    "\\\[<gdb\.Symtab.*>\\\]" \
> +    "Compunit symtabs return a sequence of gdb.Symtab"
> +
> +
> +gdb_unload "unload 1"
> +
> +gdb_test "python print (objfile.is_valid())" "False" \
> +"Get objfile validity after unload"
> +gdb_test "python print (objfile.compunits())" "RuntimeError.*: Objfile no longer exists.*" \
> +"Get objfile compunits after unload"
> \ No newline at end of file
> diff --git a/gdb/testsuite/gdb.python/py-symtab.exp b/gdb/testsuite/gdb.python/py-symtab.exp
> index 9caa5f1ebad..a949bf1e3b7 100644
> --- a/gdb/testsuite/gdb.python/py-symtab.exp
> +++ b/gdb/testsuite/gdb.python/py-symtab.exp
> @@ -68,6 +68,8 @@ gdb_test "python print (sal.is_valid())" "True" "test sal.is_valid"
>  gdb_test "python print (symtab.filename)" ".*${py_symbol_c}" "test symtab.filename"
>  gdb_test "python print (symtab.objfile)" \
>      "<gdb.Objfile filename=.*${testfile}.*>" "test symtab.objfile"
> +gdb_test "python print (symtab.compunit)" \
> +    "<gdb.Compunit .*>" "test symtab.compunit"
>  gdb_test "python print (symtab.fullname())" ".*${full_py_symbol_c}" "test symtab.fullname"
>  gdb_test "python print (symtab.is_valid())" "True" "test symtab.is_valid()"
>  gdb_test "python print (\"qq\" in global_symbols)" "True" "test qq in global symbols"
> @@ -110,6 +112,12 @@ gdb_test "python print (symtab != 123 )"\
>      "True" \
>      "test symtab non-equality with non-symtab"
>  
> +gdb_test "python print (symtab.compunit in symtab.objfile.compunits())" \
> +    "True" "Test symtab.compunit in symtab.objfile.compunits()"
> +gdb_test "python print (symtab.compunit.global_block() is symtab.global_block())" \
> +    "True" "Test symtab.compunit.global_block() is symtab.global_block()"
> +gdb_test "python print (symtab.compunit.static_block() is symtab.static_block())" \
> +    "True" "Test symtab.compunit.static_block() is symtab.static_block()"
>  
>  # Test is_valid when the objfile is unloaded.  This must be the last
>  # test as it unloads the object file in GDB.
> -- 
> 2.45.2
  
Jan Vrany Jan. 14, 2025, 12:01 p.m. UTC | #3
Hi, 

On Mon, 2025-01-13 at 18:15 +0000, Andrew Burgess wrote:
> Jan Vrany <jan.vrany@labware.com> writes:
> 
> 
> 
> > +
> > +  return gdbpy_ref<>::new_reference ((PyObject * )compunit_obj);
> > +}
> 
> I don't really like the approach taken by this function.  Each time
> the
> function is called we get a new gdb.Compunit object created and
> chained
> onto the objfile.
> 
> Now, I know that the use of gdbpy_richcompare means that the
> different
> objects will compare as equal.  But I think it would be better to
> just
> reuse the existing objects, i.e. have this function search the chain
> of
> objects looking for a match.

I'm with you - I'm not a big fan of this approach either. But: this is
exactly how other objfile-owned Python objects are implemented,
gdb.Symtab, gdb.Symbol. It was also the case for gdb.Type until Tom
changed it not long ago. And this is why I needed gdbpy_richcompare.

In fact, in an early iteration of this code I did something along the
lines of what you suggested but then decided not to do that and 
just fall in line with the others.

I can try revive that code and convert (at least) gdb.Symtab and
gdb.Symbol in a separate series (this one feels little-too-
big already) and once done, get back to this series. This would 
also make it easier to add new objects in the future (gdb.Section
and gdb.MinimalSymbol come to mind).

> 
> Additionally, and I didn't know if this was intentional or not,
> PyObject_NEW returns a new reference, i.e. the object returned has a
> refcount of 1 already.  So why you return the
> gdbpy_ref<>::new_reference(), the object you return has a refcount of
> 2.
> 
> What this means is that cupy_dealloc is never called as each
> compunit_object chained off the objfile has a base refcount of 1, and
> when cupy_deleter::operator() is called, I think we end up leaking
> the
> Python objects.

Yeah, this is a bug. Thanks! 

Thanks! 

Jan
  

Patch

diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index ecb323d8f02..88566b1d41a 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -438,6 +438,7 @@  SUBDIR_PYTHON_SRCS = \
 	python/py-value.c \
 	python/py-varobj.c \
 	python/py-xmethods.c \
+	python/py-compunit.c \
 	python/python.c
 
 SUBDIR_PYTHON_OBS = $(patsubst %.c,%.o,$(SUBDIR_PYTHON_SRCS))
diff --git a/gdb/NEWS b/gdb/NEWS
index 3d208744103..7273b23f989 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -94,6 +94,8 @@ 
   ** Added gdb.Type.function.  Returns a new gdb.Type representing a function
      returning that type.  Parameter types can be specified too.
 
+  ** Added class gdb.Compunit.
+
 * Debugger Adapter Protocol changes
 
   ** The "scopes" request will now return a scope holding global
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 272b51d32c5..2065f001320 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -220,6 +220,7 @@  optional arguments while skipping others.  Example:
 * Blocks In Python::            Accessing blocks from Python.
 * Symbols In Python::           Python representation of symbols.
 * Symbol Tables In Python::     Python representation of symbol tables.
+* Compunits In Python::         Python representation of compunits.
 * Line Tables In Python::       Python representation of line tables.
 * Breakpoints In Python::       Manipulating breakpoints using Python.
 * Finish Breakpoints in Python:: Setting Breakpoints on function return
@@ -5691,6 +5692,11 @@  Like @code{Objfile.lookup_global_symbol}, but searches for a global
 symbol with static linkage named @var{name} in this objfile.
 @end defun
 
+@defun Objfile.compunits ()
+Return a sequence of all the compunits associated with this objfile.
+@xref{Compunits In Python}.
+@end defun
+
 @node Frames In Python
 @subsubsection Accessing inferior stack frames from Python
 
@@ -6043,6 +6049,11 @@  have a superblock that is not the static block -- for instance this
 happens for an inlined function.
 @end defvar
 
+@defvar Block.compunit
+The compunit containing this block.  @xref{Compunits In Python}.
+This attribute is not writable.
+@end defvar
+
 @defvar Block.superblock
 The block containing this block.  If this parent block does not exist,
 this attribute holds @code{None}.  This attribute is not writable.
@@ -6471,6 +6482,11 @@  If no producer information is available then @code{None} is returned.
 This attribute is not writable.
 @end defvar
 
+@defvar Symtab.compunit
+The compunit this symbol table belongs to.  @xref{Compunits In Python}.
+This attribute is not writable.
+@end defvar
+
 A @code{gdb.Symtab} object has the following methods:
 
 @defun Symtab.is_valid ()
@@ -6500,6 +6516,59 @@  Return the line table associated with the symbol table.
 @xref{Line Tables In Python}.
 @end defun
 
+@node Compunits In Python
+@subsubsection Compunits representation in Python
+
+@cindex compunits in python
+@tindex gdb.Compunit
+
+Access to compunits maintained by @value{GDBN} on objfiles
+is exposed to Python via @code{gdb.Compunit}.  Compunit for a symbol table can
+be accessed via @code{compunit} property of @code{gdb.Symtab} object.
+@xref{Symbol Tables In Python}.  Method @code{compunits} of
+@code{gdb.Objfile} can be used to get a list of all compunits belonging to
+that objfile.  @xref{Objfiles In Python}.
+
+A @code{gdb.Compunit} object has the following attributes:
+
+@defvar Compunit.objfile
+The compunits' backing object file.  @xref{Objfiles In Python}.
+This attribute is not writable.
+@end defvar
+
+@defvar Compunit.producer
+The name and possibly version number of the program that
+compiled the code in the compunit.
+The contents of this string is up to the compiler.
+If no producer information is available then @code{None} is returned.
+This attribute is not writable.
+@end defvar
+
+@defvar Compunit.symtabs
+The list of symbol tables associated with this compunit.
+@xref{Symbol Tables In Python}.  This attribute is not writable.
+@end defvar
+
+A @code{gdb.Compunit} object has the following methods:
+
+@defun Compunit.is_valid ()
+Returns @code{True} if the @code{gdb.Compunit} object is valid,
+@code{False} if not.  A @code{gdb.Compunit} object can become invalid if
+the compunit it refers to does not exist in @value{GDBN} any
+longer.  All other @code{gdb.Compunit} methods will throw an exception
+if it is invalid at the time the method is called.
+@end defun
+
+@defun Compunit.global_block ()
+Return the global block of the underlying compunit.
+@xref{Blocks In Python}.
+@end defun
+
+@defun Compunit.static_block ()
+Return the static block of the underlying compunit.
+@xref{Blocks In Python}.
+@end defun
+
 @node Line Tables In Python
 @subsubsection Manipulating line tables using Python
 
diff --git a/gdb/python/py-block.c b/gdb/python/py-block.c
index 1c5eab44b3a..a05a2796c52 100644
--- a/gdb/python/py-block.c
+++ b/gdb/python/py-block.c
@@ -215,6 +215,19 @@  blpy_get_static_block (PyObject *self, void *closure)
   return block_to_block_object (static_block, self_obj->objfile);
 }
 
+/* Getter function for Block.compunit.  */
+
+static PyObject *
+blpy_get_compunit (PyObject *self, void *closure)
+{
+  const struct block *block;
+
+  BLPY_REQUIRE_VALID (self, block);
+
+  return compunit_to_compunit_object (
+	   block->global_block ()->compunit ()).release ();
+}
+
 /* Implementation of gdb.Block.is_global (self) -> Boolean.
    Returns True if this block object is a global block.  */
 
@@ -553,6 +566,8 @@  static gdb_PyGetSetDef block_object_getset[] = {
     "Block containing the global block.", NULL },
   { "static_block", blpy_get_static_block, NULL,
     "Block containing the static block.", NULL },
+  { "compunit", blpy_get_compunit, nullptr,
+    "Compunit containing this block.", nullptr },
   { "is_static", blpy_is_static, NULL,
     "Whether this block is a static block.", NULL },
   { "is_global", blpy_is_global, NULL,
diff --git a/gdb/python/py-compunit.c b/gdb/python/py-compunit.c
new file mode 100644
index 00000000000..829746cc92d
--- /dev/null
+++ b/gdb/python/py-compunit.c
@@ -0,0 +1,319 @@ 
+/* Python interface to compunits.
+
+   Copyright (C) 2008-2024 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 "charset.h"
+#include "symtab.h"
+#include "source.h"
+#include "python-internal.h"
+#include "objfiles.h"
+#include "block.h"
+
+struct compunit_object {
+  PyObject_HEAD
+
+  /* The GDB compunit structure.  */
+  struct compunit_symtab *compunit;
+
+  /* A compunit object is associated with an objfile, so keep track with
+     a doubly-linked list, rooted in the objfile.  This allows
+     invalidation of the underlying struct compunit_symtab when the objfile is
+     deleted.  */
+  compunit_object *prev;
+  compunit_object *next;
+};
+
+/* This function is called when an objfile is about to be freed.
+   Invalidate the compunit as further actions on the compunit
+   would result in bad data.  All access to obj->compunit should be
+   gated by CUPY_REQUIRE_VALID which will raise an exception on
+   compunits.  */
+struct cupy_deleter
+{
+  void operator() (compunit_object *obj)
+  {
+    while (obj)
+      {
+	compunit_object *next = obj->next;
+
+	obj->compunit = nullptr;
+	obj->next = nullptr;
+	obj->prev = nullptr;
+	obj = next;
+      }
+  }
+};
+
+extern PyTypeObject compunit_object_type
+    CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("compunit_object");
+static const registry<objfile>::key<compunit_object, cupy_deleter>
+     cupy_objfile_data_key;
+
+/* Require a valid compunit.  All access to compunit_object->compunit
+   should be gated by this call.  */
+
+#define CUPY_REQUIRE_VALID(compunit_obj, compunit)		 \
+  do {							 \
+    compunit = compunit_object_to_compunit (compunit_obj);	 \
+    if (compunit == nullptr)					 \
+      {							 \
+	PyErr_SetString (PyExc_RuntimeError,		 \
+			 _("Compunit object is invalid.")); \
+	return nullptr;					 \
+      }							 \
+  } while (0)
+
+
+/* Getter function for gdb.Compunit.objfile.  */
+
+static PyObject *
+cupy_get_objfile (PyObject *self, void *closure)
+{
+  struct compunit_symtab *compunit = nullptr;
+
+  CUPY_REQUIRE_VALID (self, compunit);
+
+  return objfile_to_objfile_object (compunit->objfile ()).release ();
+}
+
+/* Getter function for gdb.Compunit.producer.  */
+
+static PyObject *
+cupy_get_producer (PyObject *self, void *closure)
+{
+  struct compunit_symtab *compunit = nullptr;
+
+  CUPY_REQUIRE_VALID (self, compunit);
+  if (compunit->producer () != nullptr)
+    {
+      const char *producer = compunit->producer ();
+
+      return host_string_to_python_string (producer).release ();
+    }
+
+  Py_RETURN_NONE;
+}
+
+/* Implementation of gdb.Compunit.is_valid (self) -> Boolean.
+   Returns True if this Symbol table still exists in GDB.  */
+
+static PyObject *
+cupy_is_valid (PyObject *self, PyObject *args)
+{
+  struct compunit_symtab *compunit = nullptr;
+
+  compunit = compunit_object_to_compunit (self);
+  if (compunit == nullptr)
+    Py_RETURN_FALSE;
+
+  Py_RETURN_TRUE;
+}
+
+/* Return the GLOBAL_BLOCK of the underlying compunit.  */
+
+static PyObject *
+cupy_global_block (PyObject *self, PyObject *args)
+{
+  struct compunit_symtab *compunit = nullptr;
+  const struct blockvector *blockvector;
+
+  CUPY_REQUIRE_VALID (self, compunit);
+
+  blockvector = compunit->blockvector ();
+  const struct block *block = blockvector->global_block ();
+
+  return block_to_block_object (block, compunit->objfile ());
+}
+
+/* Return the STATIC_BLOCK of the underlying compunit.  */
+
+static PyObject *
+cupy_static_block (PyObject *self, PyObject *args)
+{
+  struct compunit_symtab *compunit = nullptr;
+  const struct blockvector *blockvector;
+
+  CUPY_REQUIRE_VALID (self, compunit);
+
+  blockvector = compunit->blockvector ();
+  const struct block *block = blockvector->static_block ();
+
+  return block_to_block_object (block, compunit->objfile ());
+}
+
+static PyObject *
+cupy_get_symtabs (PyObject *self, void *closure)
+{
+  struct compunit_symtab *compunit = nullptr;
+
+  CUPY_REQUIRE_VALID (self, compunit);
+
+  gdbpy_ref<> list (PyList_New (0));
+  if (list == nullptr)
+    return nullptr;
+
+  for (struct symtab *each : compunit->filetabs ())
+    {
+      {
+	gdbpy_ref<> item (symtab_to_symtab_object (each));
+	if (item.get () == nullptr
+	      || PyList_Append (list.get (), item.get ()) == -1)
+	    return nullptr;
+	}
+    }
+
+  return list.release ();
+}
+
+static void
+cupy_dealloc (PyObject *obj)
+{
+  compunit_object *compunit = (compunit_object *) obj;
+
+  if (compunit->prev)
+    compunit->prev->next = compunit->next;
+  else if (compunit->compunit)
+    cupy_objfile_data_key.set (compunit->compunit->objfile (),
+			       compunit->next);
+  if (compunit->next)
+    compunit->next->prev = compunit->prev;
+  compunit->compunit = nullptr;
+  Py_TYPE (obj)->tp_free (obj);
+}
+
+/* Given a compunit, and a compunit_object that has previously been
+   allocated and initialized, populate the compunit_object with the
+   struct compunit_symtab data.  Also, register the compunit_object life-cycle
+   with the life-cycle of the object file associated with this
+   compunit, if needed.  */
+static void
+set_compunit (compunit_object *obj, struct compunit_symtab *compunit)
+{
+  obj->compunit = compunit;
+  obj->prev = nullptr;
+  if (compunit)
+    {
+      obj->next = cupy_objfile_data_key.get (compunit->objfile ());
+      if (obj->next)
+	obj->next->prev = obj;
+      cupy_objfile_data_key.set (compunit->objfile (), obj);
+    }
+  else
+    obj->next = nullptr;
+}
+
+/* Return a new reference to gdb.Compunit Python object representing
+   COMPUNIT.  Return NULL and set the Python error on failure.  */
+gdbpy_ref<>
+compunit_to_compunit_object (struct compunit_symtab *compunit)
+{
+  compunit_object *compunit_obj;
+
+  compunit_obj = PyObject_NEW(compunit_object, &compunit_object_type);
+  if (compunit_obj)
+    set_compunit(compunit_obj, compunit);
+
+  return gdbpy_ref<>::new_reference ((PyObject * )compunit_obj);
+}
+
+/* Return struct compunit_symtab reference that is wrapped by this object.  */
+struct compunit_symtab *
+compunit_object_to_compunit (PyObject *obj)
+{
+  if (! PyObject_TypeCheck (obj, &compunit_object_type))
+    return nullptr;
+  return ((compunit_object *) obj)->compunit;
+}
+
+static int CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION
+gdbpy_initialize_compunits (void)
+{
+  if (gdbpy_type_ready (&compunit_object_type) < 0)
+    return -1;
+
+  return 0;
+}
+
+GDBPY_INITIALIZE_FILE (gdbpy_initialize_compunits);
+
+
+
+static gdb_PyGetSetDef compunit_object_getset[] = {
+  { "objfile", cupy_get_objfile, nullptr, "The compunit's objfile.",
+    nullptr },
+  { "producer", cupy_get_producer, nullptr,
+    "The name/version of the program that compiled this compunit.", nullptr },
+  { "symtabs", cupy_get_symtabs, nullptr,
+    "List of symbol tables associated with this compunit", nullptr },
+  {nullptr}  /* Sentinel */
+};
+
+static PyMethodDef compunit_object_methods[] = {
+  { "is_valid", cupy_is_valid, METH_NOARGS,
+    "is_valid () -> Boolean.\n\
+Return true if this compunit is valid, false if not." },
+  { "global_block", cupy_global_block, METH_NOARGS,
+    "global_block () -> gdb.Block.\n\
+Return the global block of the compunit." },
+  { "static_block", cupy_static_block, METH_NOARGS,
+    "static_block () -> gdb.Block.\n\
+Return the static block of the compunit." },
+  {nullptr}  /* Sentinel */
+};
+
+PyTypeObject compunit_object_type = {
+  PyVarObject_HEAD_INIT (nullptr, 0)
+  "gdb.Compunit",		  /*tp_name*/
+  sizeof (compunit_object),	  /*tp_basicsize*/
+  0,				  /*tp_itemsize*/
+  cupy_dealloc,			  /*tp_dealloc*/
+  0,				  /*tp_print*/
+  0,				  /*tp_getattr*/
+  0,				  /*tp_setattr*/
+  0,				  /*tp_compare*/
+  0,				  /*tp_repr*/
+  0,				  /*tp_as_number*/
+  0,				  /*tp_as_sequence*/
+  0,				  /*tp_as_mapping*/
+  0,				  /*tp_hash */
+  0,				  /*tp_call*/
+  0,			          /*tp_str*/
+  0,				  /*tp_getattro*/
+  0,				  /*tp_setattro*/
+  0,				  /*tp_as_buffer*/
+  Py_TPFLAGS_DEFAULT,		  /*tp_flags*/
+  "GDB compunit object",	  /*tp_doc */
+  0,				  /*tp_traverse */
+  0,				  /*tp_clear */
+  gdbpy_richcompare<compunit_object, compunit_symtab,
+      &compunit_object::compunit>,/*tp_richcompare */
+  0,				  /*tp_weaklistoffset */
+  0,				  /*tp_iter */
+  0,				  /*tp_iternext */
+  compunit_object_methods,	  /*tp_methods */
+  0,				  /*tp_members */
+  compunit_object_getset,	  /*tp_getset */
+  0,				  /* tp_base */
+  0,				  /* tp_dict */
+  0,				  /* tp_descr_get */
+  0,				  /* tp_descr_set */
+  0,				  /* tp_dictoffset */
+  0,				  /* tp_init */
+  0,				  /* tp_alloc */
+  PyType_GenericNew		  /* tp_new */
+};
diff --git a/gdb/python/py-objfile.c b/gdb/python/py-objfile.c
index 6ce58a1f6ed..e006fcc6e75 100644
--- a/gdb/python/py-objfile.c
+++ b/gdb/python/py-objfile.c
@@ -544,6 +544,31 @@  objfpy_repr (PyObject *self_)
 			       objfile_name (obj));
 }
 
+/* Implementation of gdb.Objfile.compunits() -> List  */
+
+static PyObject *
+objfpy_compunits (PyObject *self_, PyObject *args)
+{
+  objfile_object *self = (objfile_object *) self_;
+
+  OBJFPY_REQUIRE_VALID (self);
+
+  gdbpy_ref<> list (PyList_New (0));
+  if (list == nullptr)
+    return nullptr;
+
+  for (struct compunit_symtab *compunit : self->objfile->compunits ())
+    {
+      gdbpy_ref<> item = compunit_to_compunit_object (compunit);
+
+      if (item.get () == nullptr
+	    || PyList_Append (list.get (), item.get ()) == -1)
+	return nullptr;
+    }
+
+  return list.release ();
+}
+
 /* Subroutine of gdbpy_lookup_objfile_by_build_id to simplify it.
    Return non-zero if STRING is a potentially valid build id.  */
 
@@ -737,6 +762,10 @@  Look up a global symbol in this objfile and return it." },
     "lookup_static_symbol (name [, domain]).\n\
 Look up a static-linkage global symbol in this objfile and return it." },
 
+  { "compunits", objfpy_compunits, METH_NOARGS,
+    "compunits () -> List.\n\
+Return a sequence of compunits associated to this objfile." },
+
   { NULL }
 };
 
diff --git a/gdb/python/py-symtab.c b/gdb/python/py-symtab.c
index 5330b15ef8d..dcb78e0045f 100644
--- a/gdb/python/py-symtab.c
+++ b/gdb/python/py-symtab.c
@@ -193,6 +193,18 @@  stpy_get_producer (PyObject *self, void *closure)
   Py_RETURN_NONE;
 }
 
+/* Getter function for Symtab.compunit.  */
+
+static PyObject *
+stpy_get_compunit (PyObject *self, void *closure)
+{
+  struct symtab *symtab = nullptr;
+
+  STPY_REQUIRE_VALID (self, symtab);
+
+  return compunit_to_compunit_object (symtab->compunit ()).release ();
+}
+
 static PyObject *
 stpy_fullname (PyObject *self, PyObject *args)
 {
@@ -533,6 +545,8 @@  static gdb_PyGetSetDef symtab_object_getset[] = {
     NULL },
   { "producer", stpy_get_producer, NULL,
     "The name/version of the program that compiled this symtab.", NULL },
+  { "compunit", stpy_get_compunit, nullptr,
+    "The compunit this symtab belongs to.", nullptr },
   {NULL}  /* Sentinel */
 };
 
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 4d4810dc4cc..0a52cb86b4a 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -555,6 +555,8 @@  gdbpy_ref<thread_object> create_thread_object (struct thread_info *tp);
 gdbpy_ref<> thread_to_thread_object (thread_info *thr);;
 gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf);
 
+gdbpy_ref<> compunit_to_compunit_object (struct compunit_symtab *compunit);
+
 PyObject *gdbpy_buffer_to_membuf (gdb::unique_xmalloc_ptr<gdb_byte> buffer,
 				  CORE_ADDR address, ULONGEST length);
 
@@ -571,6 +573,7 @@  struct symtab *symtab_object_to_symtab (PyObject *obj);
 struct symtab_and_line *sal_object_to_symtab_and_line (PyObject *obj);
 frame_info_ptr frame_object_to_frame_info (PyObject *frame_obj);
 struct gdbarch *arch_object_to_gdbarch (PyObject *obj);
+struct compunit_symtab *compunit_object_to_compunit (PyObject *obj);
 
 extern PyObject *gdbpy_execute_mi_command (PyObject *self, PyObject *args,
 					   PyObject *kw);
diff --git a/gdb/testsuite/gdb.python/py-block.exp b/gdb/testsuite/gdb.python/py-block.exp
index 20f21711126..64ee12786c4 100644
--- a/gdb/testsuite/gdb.python/py-block.exp
+++ b/gdb/testsuite/gdb.python/py-block.exp
@@ -104,6 +104,8 @@  gdb_test "python print (repr (block))" \
     "Check block in many_locals_func"
 gdb_test "python print (block.function)" "many_locals_func" \
     "many_locals_func block"
+gdb_test "python print(block.compunit)" "<gdb\.Compunit object at .*>" \
+    "test compunit property"
 
 # Switch frames, then test for main block.
 gdb_test "up" ".*"
diff --git a/gdb/testsuite/gdb.python/py-compunit.exp b/gdb/testsuite/gdb.python/py-compunit.exp
new file mode 100644
index 00000000000..450cd6c9f1b
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-compunit.exp
@@ -0,0 +1,62 @@ 
+# Copyright (C) 2024-2024 Free Software Foundation, Inc.
+
+# 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/>.
+
+# This file is part of the GDB testsuite.  It tests the compunit
+# support in Python.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile py-objfile.c
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
+    return -1
+}
+
+if {![runto_main]} {
+    return 0
+}
+
+set python_error_text "Error occurred in Python.*"
+
+gdb_py_test_silent_cmd "python sym = gdb.lookup_symbol(\"some_var\")" \
+    "Find a symbol in objfile" 1
+gdb_py_test_silent_cmd "python objfile = sym\[0\].symtab.objfile" \
+    "Get backing object file" 1
+
+gdb_test "python print (len(objfile.compunits()) > 0)" \
+    "True" \
+    "Get objfile compunits"
+gdb_test "python print (objfile.compunits())" \
+    "\\\[<gdb\.Compunit object at .*>\\\]" \
+    "Objfile compunits return a sequence of gdb.Compunit"
+gdb_test "python print (objfile.compunits()\[0\] == objfile.compunits()\[0\])" \
+    "True" \
+    "Compunits are comparable"
+gdb_test "python print (len(objfile.compunits()\[0\].symtabs) > 0)" \
+    "True" \
+    "Get compunit symtabs"
+gdb_test "python print (objfile.compunits()\[0\].symtabs)" \
+    "\\\[<gdb\.Symtab.*>\\\]" \
+    "Compunit symtabs return a sequence of gdb.Symtab"
+
+
+gdb_unload "unload 1"
+
+gdb_test "python print (objfile.is_valid())" "False" \
+"Get objfile validity after unload"
+gdb_test "python print (objfile.compunits())" "RuntimeError.*: Objfile no longer exists.*" \
+"Get objfile compunits after unload"
\ No newline at end of file
diff --git a/gdb/testsuite/gdb.python/py-symtab.exp b/gdb/testsuite/gdb.python/py-symtab.exp
index 9caa5f1ebad..a949bf1e3b7 100644
--- a/gdb/testsuite/gdb.python/py-symtab.exp
+++ b/gdb/testsuite/gdb.python/py-symtab.exp
@@ -68,6 +68,8 @@  gdb_test "python print (sal.is_valid())" "True" "test sal.is_valid"
 gdb_test "python print (symtab.filename)" ".*${py_symbol_c}" "test symtab.filename"
 gdb_test "python print (symtab.objfile)" \
     "<gdb.Objfile filename=.*${testfile}.*>" "test symtab.objfile"
+gdb_test "python print (symtab.compunit)" \
+    "<gdb.Compunit .*>" "test symtab.compunit"
 gdb_test "python print (symtab.fullname())" ".*${full_py_symbol_c}" "test symtab.fullname"
 gdb_test "python print (symtab.is_valid())" "True" "test symtab.is_valid()"
 gdb_test "python print (\"qq\" in global_symbols)" "True" "test qq in global symbols"
@@ -110,6 +112,12 @@  gdb_test "python print (symtab != 123 )"\
     "True" \
     "test symtab non-equality with non-symtab"
 
+gdb_test "python print (symtab.compunit in symtab.objfile.compunits())" \
+    "True" "Test symtab.compunit in symtab.objfile.compunits()"
+gdb_test "python print (symtab.compunit.global_block() is symtab.global_block())" \
+    "True" "Test symtab.compunit.global_block() is symtab.global_block()"
+gdb_test "python print (symtab.compunit.static_block() is symtab.static_block())" \
+    "True" "Test symtab.compunit.static_block() is symtab.static_block()"
 
 # Test is_valid when the objfile is unloaded.  This must be the last
 # test as it unloads the object file in GDB.