[PATCH 0/5] create GDB/MI commands using python

Andrew Burgess aburgess@redhat.com
Tue Jan 18 13:55:41 GMT 2022


* Jan Vrany via Gdb-patches <gdb-patches@sourceware.org> [2022-01-17 12:44:20 +0000]:

> This is a restart of an earlier attempts to allow custom
> GDB/MI commands written in Python.

Thanks for continuing to work on this feature.

I too had been looking at getting the remaining patches from your
series upstream, and I'd like to discuss some of the differences
between the approaches we took.

At the end of this mail you'll find my current work-in-progress
patch, it definitely needs the docs and NEWS entries adding, as well
as a few extra tests.  However, functionality wise I think its mostly
there.

My patch includes two sets of tests, gdb.python/py-mi-cmd.exp and
gdb.python/py-mi-cmd-orig.exp.  The former is based on your tests, but
with some tweaks based on changes I made.  The latter set is your
tests taken from this m/l thread.

When running the gdb.python/py-mi-cmd-orig.exp tests there should be
just 2 failures, both related to the same feature which is not present
in my version, that is, the ability of a command to redefine itself,
like this:

      class my_command(gdb.MICommand):
        def invoke(self, args):
          my_command("-blah")
          return None

       my_command("-blah")

this works with your version, but not with mine, this is because I'm
using python's own reference counting to track when a command can be
redefined or not, and, when you are within a commands invoke method
the reference count of the containing object is incremented, and this
prevents gdb from deleting the command.

My question then, is how important is this feature, and what use case
do you see for this?  Or, was support for this just a happy side
effect of the implementation approach you chose?

Thanks,
Andrew

---

commit 0ef6ec4fbf237a89697c3e6a16888d57f4e8b777
Author: Jan Vrany <jan.vrany@labware.com>
Date:   Tue Jun 23 14:45:38 2020 +0100

    [WIP] gdb: create MI commands using python
    
    This commit allows an user to create custom MI commands using Python
    similarly to what is possible for Python CLI commands.
    
    A new subclass of mi_command is defined for Python MI commands,
    mi_command_py. A new file, py-micmd.c contains the logic for Python MI
    commands.
    
    This commit is based on work linked too from this mailing list thread:
    
      https://sourceware.org/pipermail/gdb/2021-November/049774.html
    
    Which has also been previously posted to the mailing list here:
    
      https://sourceware.org/pipermail/gdb-patches/2019-May/158010.html
    
    And was recently reposted here:
    
      https://sourceware.org/pipermail/gdb-patches/2022-January/185190.html
    
    I've made some adjustments to how this feature is implemented though.
    
    One notable change is how the lifetime of the Python gdb.MICommand
    objects is managed.  In the original patch, these object were kept
    alive by an owned reference within the mi_command_py object.  As such,
    the Python object would not be deleted until the mi_command_py object
    itself was deleted.
    
    This caused a problem, the mi_command_py were held in the global mi
    command table (in mi/mi-cmds.c), which, as a global, was not cleared
    until program shutdown.  By this point the Python interpreter has
    already been shutdown.  Attempting to delete the mi_command_py object
    at this point was cause GDB to try and invoke Python code after
    finalising the Python interpreter, and we would crash.
    
    To work around this problem, the original patch added code in
    python/python.c that would search the mi command table, and delete the
    mi_command_py objects before the Python environment was finalised.
    
    I didn't like this, it means making the mi command table global, and
    having the python.c code understand about mi commands.
    
    In my version, I store the mi commands in a dictionary in the gdb
    module.  It is this dictionary that holds the reference to the Python
    object.  When the Python environment is finalised the dictionary is
    deleted, reducing the reference count on the Python objects within,
    this in turn allows the mi command objects to be deleted.
    
    I then make it so that when a Python MICommand object is deleted, the
    corresponding mi command is automatically deleted from the mi command
    table, removing that mi command.
    
    A second change I made is to handle the case where an object is
    reinitialised, consider this code:
    
      class my_command(gdb.MICommand):
        def invoke(self, args):
          # ...
          return None
    
      cmd = my_command("-my-command")
      cmd.__init__("-changed-the-name")
    
    Though strange, this is perfectly valid Python code, this is now
    handled correctly, the old `-my-command` will be removed, and the new
    `-changed-the-name` command will be added.
    
    A third change is how command redefinition is handled.  The old series
    required special support to allow command redefinition.  Now we get
    almost the same functionality, naturally.
    
    Consider:
    
      class command_one(gdb.MICommand):
        def invoke(self, args):
          # ...
          return None
    
      class command_two(gdb.MICommand):
        def invoke(self, args):
          # ...
          return None
    
      command_one("-my-command")
      command_two("-my-command")
    
    This will now do what (I hope) you'd expect.  There will be a single
    mi command `-my-command` defined, that calls `command_two.invoke`.
    
    However, if using the same class definitions, we try this instead:
    
      c1 = command_one("-my-command")
      c2 = command_two("-my-command")
    
    The second line will now fail.  The reason here is that `c1` is still
    holding a reference to the `command_one` object, and this prevents
    `c2` from taking that command name.  If we do this:
    
      c1 = command_one("-my-command")
      c1 = None
      c2 = command_two("-my-command")
    
    Now we are again fine, and c2 will be correctly initialised.  This
    feels like a bit of an edge case, but it's something that needs
    figuring out.  A different possibility would be to always allow `c2`
    to be created.  In the first case, creating `c2` would invalidate
    `c1`.  We would then have an "is_valid" method on gdb.MICommand
    object, so we could write something like:
    
      c1 = command_one("-my-command")
      c2 = command_two("-my-command")
      if c1.is_valid ():
        c1.invoke([])
    
    I considered this, but rejected it.  I believe that in most cases
    users will not be assigning the created MICommand objects to a
    variable, as it doesn't really serve any purpose.
    
    A fourth change is how the memory is handled for the name of the
    python mi command.  In the recently posted series the mi_command class
    has the name removed, and the name accessor is now virtual.  It is
    then up to the sub-classes to correctly handle the memory.  Instead of
    this, I just made the Python object manage the memory.  As the Python
    object always outlives the mi_command object the mi_command object can
    simply refer to the memory within the Python object.  This avoids
    having to add more complexity to the mi_command class hierarchy.
    
    I kept the test set from the original patch, and, almost all of the
    original tests pass with my new series.  There was however, one
    feature from the original series that I decided to drop, as supporting
    it within the new scheme would have added significant complexity, for
    what I consider, very little benefit.  That feature was for allowing
    MI commands to redefine themselves, something like:
    
      class my_command(gdb.MICommand):
        def invoke(self, args):
          my_command("-blah")
          return None
    
       my_command("-blah")
    
    This would work under the old scheme, and was tested for, but, will no
    longer work.  The reason this fails is that while invoking a command,
    the invoke method itself holds a reference to the command object.  As
    a result, this turns out to be similar to the first c1/c2 case posted
    above, the my_command object is referenced from both the global
    dictionary (in the gdb module), and from the running invoke method.
    When attempting to redefine the command GDB understands that it can
    reduce the reference count by removing the entry from the dictionary
    in the gdb module, but the extra reference prevents GDB from deleting
    the previous my_command object.
    
    todo:
    =====
    
    I'm still not 100% sure if I was correct to reject the is_valid method
    approach, maybe that would be a more natural way for the API to work?

diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index d0db5fbdee1..5cb428459c3 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -409,6 +409,7 @@ SUBDIR_PYTHON_SRCS = \
 	python/py-lazy-string.c \
 	python/py-linetable.c \
 	python/py-membuf.c \
+	python/py-micmd.c \
 	python/py-newobjfileevent.c \
 	python/py-objfile.c \
 	python/py-param.c \
diff --git a/gdb/mi/mi-cmds.c b/gdb/mi/mi-cmds.c
index cd7cabdda9b..dd0243e5bfe 100644
--- a/gdb/mi/mi-cmds.c
+++ b/gdb/mi/mi-cmds.c
@@ -26,10 +26,6 @@
 #include <map>
 #include <string>
 
-/* A command held in the MI_CMD_TABLE.  */
-
-using mi_command_up = std::unique_ptr<struct mi_command>;
-
 /* MI command table (built at run time). */
 
 static std::map<std::string, mi_command_up> mi_cmd_table;
@@ -113,12 +109,12 @@ struct mi_command_cli : public mi_command
    not have been added to mi_cmd_table.  Otherwise, return true, and
    COMMAND was added to mi_cmd_table.  */
 
-static bool
+bool
 insert_mi_cmd_entry (mi_command_up command)
 {
   gdb_assert (command != nullptr);
 
-  const std::string &name = command->name ();
+  const std::string name (command->name ());
 
   if (mi_cmd_table.find (name) != mi_cmd_table.end ())
     return false;
@@ -127,6 +123,20 @@ insert_mi_cmd_entry (mi_command_up command)
   return true;
 }
 
+bool
+remove_mi_cmd_entry (mi_command *command)
+{
+  gdb_assert (command != nullptr);
+
+  const std::string name (command->name ());
+
+  if (mi_cmd_table.find (name) == mi_cmd_table.end ())
+    return false;
+
+  mi_cmd_table.erase (name);
+  return true;
+}
+
 /* Create and register a new MI command with an MI specific implementation.
    NAME must name an MI command that does not already exist, otherwise an
    assertion will trigger.  */
diff --git a/gdb/mi/mi-cmds.h b/gdb/mi/mi-cmds.h
index 2a93a9f5476..3f4fb854d68 100644
--- a/gdb/mi/mi-cmds.h
+++ b/gdb/mi/mi-cmds.h
@@ -187,6 +187,10 @@ struct mi_command
   int *m_suppress_notification;
 };
 
+/* A command held in the MI_CMD_TABLE.  */
+
+using mi_command_up = std::unique_ptr<struct mi_command>;
+
 /* Lookup a command in the MI command table, returns nullptr if COMMAND is
    not found.  */
 
@@ -194,4 +198,15 @@ extern mi_command *mi_cmd_lookup (const char *command);
 
 extern void mi_execute_command (const char *cmd, int from_tty);
 
+/* Insert a new mi-command into the command table.  Return true if
+   insertion was successful.  */
+
+extern bool insert_mi_cmd_entry (mi_command_up command);
+
+/* Remove a mi-command from the command table.  Return true if the removal
+   was success, otherwise return false.  */
+
+extern bool remove_mi_cmd_entry (mi_command *command);
+
+
 #endif /* MI_MI_CMDS_H */
diff --git a/gdb/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py
index 11a1b444bfd..a9197eb4ffe 100644
--- a/gdb/python/lib/gdb/__init__.py
+++ b/gdb/python/lib/gdb/__init__.py
@@ -81,6 +81,9 @@ frame_filters = {}
 # Initial frame unwinders.
 frame_unwinders = []
 
+# Hash containing all user created MI commands, the key is the command
+# name, and the value is the gdb.MICommand object.
+mi_commands = {}
 
 def _execute_unwinders(pending_frame):
     """Internal function called from GDB to execute all unwinders.
diff --git a/gdb/python/py-micmd.c b/gdb/python/py-micmd.c
new file mode 100644
index 00000000000..042ba624f06
--- /dev/null
+++ b/gdb/python/py-micmd.c
@@ -0,0 +1,445 @@
+/* MI Command Set for GDB, the GNU debugger.
+
+   Copyright (C) 2019 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/>.  */
+
+/* gdb MI commands implemented in Python  */
+
+#include "defs.h"
+#include "python-internal.h"
+#include "arch-utils.h"
+#include "charset.h"
+#include "language.h"
+#include "mi/mi-cmds.h"
+#include "mi/mi-parse.h"
+
+#include <string>
+
+struct mi_command_py;
+
+/* Representation of a python gdb.MICommand object.  */
+
+struct micmdpy_object
+{
+  PyObject_HEAD
+
+  /* The object representing this command in the MI command table.  */
+  struct mi_command_py *mi_command;
+
+  /* The string representing this command name.  This string is referenced
+     from the mi_command_py object, and must be free'd once the
+     mi_command_py object is no longer needed.  */
+  char *command_name;
+};
+
+/* MI command implemented in Python.  */
+
+struct mi_command_py : public mi_command
+{
+  /* Constructs a new mi_command_py object.  NAME is command name without
+     leading dash.  OBJECT is a reference to a Python object implementing
+     the command.  This object should inherit from gdb.MICommand and should
+     implement method invoke (args). */
+
+  mi_command_py (const char *name, PyObject *object)
+    : mi_command (name, nullptr),
+      m_pyobj (object)
+  { /* Nothing.  */ }
+
+  ~mi_command_py () = default;
+
+protected:
+  /* Called when the mi command is invoked.  */
+  virtual void do_invoke(struct mi_parse *parse) const override;
+
+private:
+  /* The python object representing this mi command.  */
+  PyObject *m_pyobj;
+};
+
+static PyObject *invoke_cst;
+
+extern PyTypeObject micmdpy_object_type
+    CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("micmdpy_object");
+
+/* If the command invoked returns a list, this function parses it and create an
+   appropriate MI out output.
+
+   The returned values must be Python string, and can be contained within Python
+   lists and dictionaries. It is possible to have a multiple levels of lists
+   and/or dictionaries.  */
+
+static void
+parse_mi_result (PyObject *result, const char *field_name)
+{
+  struct ui_out *uiout = current_uiout;
+
+  if (PyDict_Check (result))
+    {
+      PyObject *key, *value;
+      Py_ssize_t pos = 0;
+      ui_out_emit_tuple tuple_emitter (uiout, field_name);
+      while (PyDict_Next (result, &pos, &key, &value))
+	{
+	  if (!PyString_Check (key))
+	    {
+	      gdbpy_ref<> key_repr (PyObject_Repr (key));
+	      if (PyErr_Occurred () != NULL)
+                {
+                  gdbpy_err_fetch ex;
+                  gdb::unique_xmalloc_ptr<char> ex_msg (ex.to_string ());
+
+                  if (ex_msg == NULL || *ex_msg == '\0')
+                    error (_("Non-string object used as key."));
+                  else
+                    error (_("Non-string object used as key: %s."),
+                           ex_msg.get ());
+                }
+              else
+	        {
+	          auto key_repr_string
+	                 = python_string_to_target_string (key_repr.get ());
+	          error (_("Non-string object used as key: %s."),
+                         key_repr_string.get ());
+	        }
+	    }
+
+	  auto key_string = python_string_to_target_string (key);
+	  parse_mi_result (value, key_string.get ());
+	}
+    }
+  else if (PySequence_Check (result) && !PyString_Check (result))
+    {
+      ui_out_emit_list list_emitter (uiout, field_name);
+      for (Py_ssize_t i = 0; i < PySequence_Size (result); ++i)
+	{
+          gdbpy_ref<> item (PySequence_ITEM (result, i));
+	  parse_mi_result (item.get (), NULL);
+	}
+    }
+  else if (PyIter_Check (result))
+    {
+      gdbpy_ref<> item;
+      ui_out_emit_list list_emitter (uiout, field_name);
+      while (item.reset (PyIter_Next (result)), item != nullptr)
+	parse_mi_result (item.get (), NULL);
+    }
+  else
+    {
+      gdb::unique_xmalloc_ptr<char> string (gdbpy_obj_to_string (result));
+      uiout->field_string (field_name, string.get ());
+    }
+}
+
+/* Object initializer; sets up gdb-side structures for MI command.
+
+   Use: __init__(NAME).
+
+   NAME is the name of the MI command to register.  It must start with a dash
+   as traditional MI commands do.  */
+
+static int
+micmdpy_init (PyObject *self, PyObject *args, PyObject *kw)
+{
+  const char *name;
+  micmdpy_object *cmd = (micmdpy_object *) self;
+
+  if (!PyArg_ParseTuple (args, "s", &name))
+    return -1;
+
+  /* Validate command name */
+  const int name_len = strlen (name);
+  if (name_len == 0)
+    {
+      error (_("MI command name is empty."));
+      return -1;
+    }
+  else if ((name_len < 2) || (name[0] != '-') || !isalnum (name[1]))
+    {
+      error (_("MI command name does not start with '-'"
+               " followed by at least one letter or digit."));
+      return -1;
+    }
+  else
+    for (int i = 2; i < name_len; i++)
+      {
+	if (!isalnum (name[i]) && name[i] != '-')
+	  {
+	    error (_("MI command name contains invalid character: %c."),
+                   name[i]);
+	    return -1;
+	  }
+      }
+
+  if (!PyObject_HasAttr (self, invoke_cst))
+    error (_("-%s: Python command object missing 'invoke' method."), name);
+
+  /* Now insert the object into the dictionary that lives in the gdb module.  */
+  if (gdb_python_module == nullptr
+      || ! PyObject_HasAttrString (gdb_python_module, "mi_commands"))
+    error (_("unable to find gdb.mi_commands dictionary"));
+
+  gdbpy_ref<> mi_cmd_dict (PyObject_GetAttrString (gdb_python_module,
+						   "mi_commands"));
+  if (mi_cmd_dict == nullptr || !PyDict_Check (mi_cmd_dict.get ()))
+    error (_("unable to fetch gdb.mi_commands dictionary"));
+
+  /* Is the user re-initialising an existing gdb.MICommand object?  */
+  if (cmd->mi_command != nullptr)
+    {
+      /* Grab the old name for this command.  */
+      mi_command *old_cmd = cmd->mi_command;
+      gdbpy_ref<> old_name_obj
+	= host_string_to_python_string (old_cmd->name ());
+
+      /* Lookup the gdb.MICommand object in the dictionary of all python mi
+	 commands, this is gdb.mi_command, and remove it.  */
+      PyObject *curr = PyDict_GetItemWithError (mi_cmd_dict.get (),
+						old_name_obj.get ());
+      if (curr == nullptr && PyErr_Occurred ())
+	return -1;
+      if (curr != nullptr)
+	{
+	  /* The old command is in gdb.mi_commands, so remove it.  */
+	  if (PyDict_DelItem (mi_cmd_dict.get (), old_name_obj.get ()) < 0)
+	    return -1;
+	}
+
+      /* Now remove the old command from the gdb internal table of mi
+	 commands.  */
+      remove_mi_cmd_entry (cmd->mi_command);
+
+      /* And reset this object back to a default state.  */
+      cmd->mi_command = nullptr;
+      xfree (cmd->command_name);
+      cmd->command_name = nullptr;
+    }
+
+  /* Create the gdb internal object to represent the mi command.  */
+  gdb::unique_xmalloc_ptr<char> name_up = make_unique_xstrdup (name + 1);
+  mi_command_py *tmp_cmd = new mi_command_py (name_up.get (), self);
+  cmd->mi_command = tmp_cmd;
+  cmd->command_name = name_up.release ();
+  mi_command_up micommand (tmp_cmd);
+
+  /* Look up this command name in the gdb.mi_commands dictionary, a command
+     with this name may already exist.  */
+  gdbpy_ref<> name_obj = host_string_to_python_string (cmd->command_name);
+
+  PyObject *curr = PyDict_GetItemWithError (mi_cmd_dict.get (),
+					    name_obj.get ());
+  if (curr == nullptr && PyErr_Occurred ())
+    return -1;
+  if (curr != nullptr)
+    {
+      /* There is a command with this name already in the gdb.mi_commands
+	 dictionary.  If the reference in the dictionary is the only
+	 reference, then we allow the user to replace this with a new
+	 command.  If, however, there are other references to this mi
+	 command object, then the user will get an error.  */
+      if (!PyObject_IsInstance (curr, (PyObject *) &micmdpy_object_type))
+	{
+	  PyErr_SetString (PyExc_RuntimeError,
+			   _("unexpected object in gdb.mi_commands dictionary"));
+	  return -1;
+	}
+
+      if (Py_REFCNT (curr) > 1)
+	{
+	  PyErr_SetString (PyExc_RuntimeError,
+			   _("unable to add command, name may already be in use"));
+	  return -1;
+	}
+
+      if (PyDict_DelItem (mi_cmd_dict.get (), name_obj.get ()) < 0)
+	return -1;
+    }
+
+  /* Add the command to the gdb internal mi command table.  */
+  bool result = insert_mi_cmd_entry (std::move (micommand));
+  if (!result)
+    {
+      PyErr_SetString (PyExc_RuntimeError,
+		       _("unable to add command, name may already be in use"));
+      return -1;
+    }
+
+  /* And add the python object to the gdb.mi_commands dictionary.  */
+  if (PyDict_SetItem (mi_cmd_dict.get (), name_obj.get (), self) < 0)
+    return -1;
+
+  return 0;
+}
+
+/* Called when a gdb.MICommand object is deallocated.  */
+
+static void
+micmdpy_dealloc (PyObject *obj)
+{
+  micmdpy_object *cmd = (micmdpy_object *) obj;
+  mi_command *py_cmd = cmd->mi_command;
+
+  /* There might not be an mi_command object if the object failed to
+     initialize correctly for some reason.  If there is though, then now is
+     the time we remove the object from the gdb internal command table.  */
+  if (py_cmd != nullptr)
+    remove_mi_cmd_entry (py_cmd);
+
+  /* Free the memory that holds the command name.  */
+  xfree (cmd->command_name);
+
+  /* Finally, free the memory for this python object.  */
+  Py_TYPE (obj)->tp_free (obj);
+}
+
+/* Called when the mi command is invoked.  */
+
+void
+mi_command_py::do_invoke (struct mi_parse *parse) const
+{
+  mi_parse_argv (parse->args, parse);
+
+  if (parse->argv == NULL)
+    error (_("Problem parsing arguments: %s %s"), parse->command, parse->args);
+
+  PyObject *obj = this->m_pyobj;
+
+  gdbpy_enter enter_py (get_current_arch (), current_language);
+
+  gdb_assert (obj != nullptr);
+
+  if (!PyObject_HasAttr (obj, invoke_cst))
+      error (_("-%s: Python command object missing 'invoke' method."),
+	     name ());
+
+
+  gdbpy_ref<> argobj (PyList_New (parse->argc));
+  if (argobj == nullptr)
+    {
+      gdbpy_print_stack ();
+      error (_("-%s: failed to create the Python arguments list."),
+	     name ());
+    }
+
+  for (int i = 0; i < parse->argc; ++i)
+    {
+      gdbpy_ref<> str (PyUnicode_Decode (parse->argv[i], strlen (parse->argv[i]),
+					 host_charset (), NULL));
+      if (PyList_SetItem (argobj.get (), i, str.release ()) != 0)
+	{
+	  error (_("-%s: failed to create the Python arguments list."),
+		 name ());
+	}
+    }
+
+  gdb_assert (PyErr_Occurred () == NULL);
+  gdbpy_ref<> result (
+    PyObject_CallMethodObjArgs (obj, invoke_cst, argobj.get (), NULL));
+  if (PyErr_Occurred () != NULL)
+    {
+      gdbpy_err_fetch ex;
+      gdb::unique_xmalloc_ptr<char> ex_msg (ex.to_string ());
+
+      if (ex_msg == NULL || *ex_msg == '\0')
+	error (_("-%s: failed to execute command"), name ());
+      else
+	error (_("-%s: %s"), name (), ex_msg.get ());
+    }
+  else
+    {
+      if (Py_None != result)
+	parse_mi_result (result.get (), "result");
+    }
+}
+
+/* Initialize the MI command object.  */
+
+int
+gdbpy_initialize_micommands ()
+{
+  micmdpy_object_type.tp_new = PyType_GenericNew;
+  if (PyType_Ready (&micmdpy_object_type) < 0)
+    return -1;
+
+  if (gdb_pymodule_addobject (gdb_module, "MICommand",
+			      (PyObject *) &micmdpy_object_type)
+      < 0)
+    return -1;
+
+  invoke_cst = PyString_FromString ("invoke");
+  if (invoke_cst == NULL)
+    return -1;
+
+  return 0;
+}
+
+/* Implement gdb.MICommand.name attribute, return a string, the name of
+   this MI command.  */
+
+static PyObject *
+micmdpy_get_name (PyObject *self, void *closure)
+{
+  struct micmdpy_object *micmd_obj = (struct micmdpy_object *) self;
+
+  gdb_assert (micmd_obj->command_name != nullptr);
+  return PyString_FromString (micmd_obj->command_name);
+}
+
+/* gdb.MICommand attributes.   */
+static gdb_PyGetSetDef micmdpy_object_getset[] = {
+  { "name", micmdpy_get_name, nullptr, "The command's name.", nullptr },
+  { nullptr }	/* Sentinel.  */
+};
+
+PyTypeObject micmdpy_object_type = {
+  PyVarObject_HEAD_INIT (NULL, 0) "gdb.MICommand", /*tp_name */
+  sizeof (micmdpy_object),			   /*tp_basicsize */
+  0,						   /*tp_itemsize */
+  micmdpy_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 | Py_TPFLAGS_BASETYPE,	/*tp_flags */
+  "GDB mi-command object",			   /* tp_doc */
+  0,						   /* tp_traverse */
+  0,						   /* tp_clear */
+  0,						   /* tp_richcompare */
+  0,						   /* tp_weaklistoffset */
+  0,						   /* tp_iter */
+  0,						   /* tp_iternext */
+  0,						   /* tp_methods */
+  0,						   /* tp_members */
+  micmdpy_object_getset,			   /* tp_getset */
+  0,						   /* tp_base */
+  0,						   /* tp_dict */
+  0,						   /* tp_descr_get */
+  0,						   /* tp_descr_set */
+  0,						   /* tp_dictoffset */
+  micmdpy_init,					   /* tp_init */
+  0,						   /* tp_alloc */
+};
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 583989c5a6d..d5a0b4a4a91 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -561,6 +561,8 @@ int gdbpy_initialize_membuf ()
   CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
 int gdbpy_initialize_connection ()
   CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
+int gdbpy_initialize_micommands (void)
+  CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
 
 /* A wrapper for PyErr_Fetch that handles reference counting for the
    caller.  */
diff --git a/gdb/python/python.c b/gdb/python/python.c
index 4dcda53d9ab..f2b09916374 100644
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -1887,7 +1887,8 @@ do_start_initialization ()
       || gdbpy_initialize_unwind () < 0
       || gdbpy_initialize_membuf () < 0
       || gdbpy_initialize_connection () < 0
-      || gdbpy_initialize_tui () < 0)
+      || gdbpy_initialize_tui () < 0
+      || gdbpy_initialize_micommands () < 0)
     return false;
 
 #define GDB_PY_DEFINE_EVENT_TYPE(name, py_name, doc, base)	\
diff --git a/gdb/testsuite/gdb.python/py-mi-cmd-orig.exp b/gdb/testsuite/gdb.python/py-mi-cmd-orig.exp
new file mode 100644
index 00000000000..78baf5e97ff
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-mi-cmd-orig.exp
@@ -0,0 +1,133 @@
+# Copyright (C) 2019-2021 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/>.
+
+# Test custom MI commands implemented in Python.
+
+load_lib gdb-python.exp
+load_lib mi-support.exp
+set MIFLAGS "-i=mi2"
+
+gdb_exit
+if {[mi_gdb_start]} {
+    continue
+}
+
+if {[lsearch -exact [mi_get_features] python] < 0} {
+    unsupported "python support is disabled"
+    return -1
+}
+
+standard_testfile
+
+#
+# Start here
+#
+
+
+mi_gdb_test "set python print-stack full" \
+  ".*\\^done" \
+  "set python print-stack full"
+
+mi_gdb_test "source ${srcdir}/${subdir}/${testfile}.py" \
+  ".*\\^done" \
+  "load python file"
+
+mi_gdb_test "python pycmd1('-pycmd')" \
+  ".*\\^done" \
+  "define -pycmd MI command"
+
+
+mi_gdb_test "-pycmd int" \
+  "\\^done,result=\"42\"" \
+  "-pycmd int"
+
+mi_gdb_test "-pycmd str" \
+  "\\^done,result=\"Hello world!\"" \
+  "-pycmd str"
+
+mi_gdb_test "-pycmd ary" \
+  "\\^done,result=\\\[\"Hello\",\"42\"\\\]" \
+  "-pycmd ary"
+
+mi_gdb_test "-pycmd dct" \
+  "\\^done,result={hello=\"world\",times=\"42\"}" \
+  "-pycmd dct"
+
+mi_gdb_test "-pycmd bk1" \
+  "\\^error,msg=\"Non-string object used as key: Bad Key\\.\"" \
+  "-pycmd bk1"
+
+mi_gdb_test "-pycmd bk2" \
+  "\\^error,msg=\"Non-string object used as key: 1\\.\"" \
+  "-pycmd bk2"
+
+mi_gdb_test "-pycmd bk3" \
+  "\\^error,msg=\"Non-string object used as key: __repr__ returned non-string .*" \
+  "-pycmd bk3"
+
+mi_gdb_test "-pycmd tpl" \
+  "\\^done,result=\\\[\"42\",\"Hello\"\\\]" \
+  "-pycmd tpl"
+
+mi_gdb_test "-pycmd itr" \
+  "\\^done,result=\\\[\"1\",\"2\",\"3\"\\\]" \
+  "-pycmd itr"
+
+mi_gdb_test "-pycmd nn1" \
+  "\\^done" \
+  "-pycmd nn1"
+
+mi_gdb_test "-pycmd nn2" \
+  "\\^done,result=\\\[\"None\"\\\]" \
+  "-pycmd nn2"
+
+mi_gdb_test "-pycmd bogus" \
+  "\\^error,msg=\"-pycmd: Invalid parameter: bogus\"" \
+  "-pycmd bogus"
+
+mi_gdb_test "-pycmd exp" \
+  "\\^error,msg=\"-pycmd: failed to execute command\"" \
+  "-pycmd exp"
+
+mi_gdb_test "python pycmd2('-pycmd')" \
+  ".*\\^done" \
+  "redefine -pycmd MI command from CLI command"
+
+mi_gdb_test "-pycmd str" \
+  "\\^done,result=\"Ciao!\"" \
+  "-pycmd str - redefined from CLI"
+
+mi_gdb_test "-pycmd int" \
+  "\\^error,msg=\"-pycmd: Invalid parameter: int\"" \
+  "-pycmd int - redefined from CLI"
+
+mi_gdb_test "-pycmd red" \
+    "\\^error,msg=\"-pycmd: Command redefined but we failing anyway\"" \
+  "redefine -pycmd MI command from Python MI command"
+
+mi_gdb_test "-pycmd int" \
+  "\\^done,result=\"42\"" \
+  "-pycmd int - redefined from MI"
+
+mi_gdb_test "python pycmd1('')" \
+  ".*\\^error,msg=\"MI command name is empty.\"" \
+  "empty MI command name"
+
+mi_gdb_test "python pycmd1('-')" \
+  ".*\\^error,msg=\"MI command name does not start with '-' followed by at least one letter or digit\\.\"" \
+  "invalid MI command name"
+
+mi_gdb_test "python pycmd1('-bad-character-@')" \
+  ".*\\^error,msg=\"MI command name contains invalid character: @\\.\"" \
+  "invalid character in MI command name"
diff --git a/gdb/testsuite/gdb.python/py-mi-cmd-orig.py b/gdb/testsuite/gdb.python/py-mi-cmd-orig.py
new file mode 100644
index 00000000000..2f6ba2f8037
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-mi-cmd-orig.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2019-2021 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/>.
+
+import gdb
+
+class BadKey:
+    def __repr__(self):
+        return "Bad Key"
+
+class ReallyBadKey:
+    def __repr__(self):
+        return BadKey()
+
+
+class pycmd1(gdb.MICommand):
+    def invoke(self, argv):
+        if argv[0] == 'int':
+            return 42
+        elif argv[0] == 'str':
+            return "Hello world!"
+        elif argv[0] == 'ary':
+            return [ 'Hello', 42 ]
+        elif argv[0] == "dct":
+            return { 'hello' : 'world', 'times' : 42}
+        elif argv[0] == "bk1":
+            return { BadKey() : 'world' }
+        elif argv[0] == "bk2":
+            return { 1 : 'world' }
+        elif argv[0] == "bk3":
+            return { ReallyBadKey() : 'world' }
+        elif argv[0] == 'tpl':
+            return ( 42 , 'Hello' )
+        elif argv[0] == 'itr':
+            return iter([1,2,3])
+        elif argv[0] == 'nn1':
+            return None
+        elif argv[0] == 'nn2':
+            return [ None ]
+        elif argv[0] == 'red':
+            pycmd2('-pycmd')
+            return None
+        elif argv[0] == 'exp':
+            raise gdb.GdbError()
+        else:
+            raise gdb.GdbError("Invalid parameter: %s" % argv[0])
+
+
+class pycmd2(gdb.MICommand):
+    def invoke(self, argv):
+        if argv[0] == 'str':
+            return "Ciao!"
+        elif argv[0] == 'red':
+            pycmd1('-pycmd')
+            raise gdb.GdbError("Command redefined but we failing anyway")
+        else:
+            raise gdb.GdbError("Invalid parameter: %s" % argv[0])
+
diff --git a/gdb/testsuite/gdb.python/py-mi-cmd.exp b/gdb/testsuite/gdb.python/py-mi-cmd.exp
new file mode 100644
index 00000000000..55a4d821351
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-mi-cmd.exp
@@ -0,0 +1,136 @@
+# Copyright (C) 2018 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/>.
+
+# Test custom MI commands implemented in Python.
+
+load_lib gdb-python.exp
+load_lib mi-support.exp
+set MIFLAGS "-i=mi2"
+
+gdb_exit
+if {[mi_gdb_start]} {
+    continue
+}
+
+if {[lsearch -exact [mi_get_features] python] < 0} {
+    unsupported "python support is disabled"
+    return -1
+}
+
+standard_testfile
+#
+# Start here
+#
+
+
+mi_gdb_test "set python print-stack full" \
+  ".*\\^done" \
+  "set python print-stack full"
+
+mi_gdb_test "source ${srcdir}/${subdir}/${testfile}.py" \
+  ".*\\^done" \
+  "load python file"
+
+mi_gdb_test "python pycmd1('-pycmd')" \
+  ".*\\^done" \
+  "Define -pycmd MI command"
+
+
+mi_gdb_test "-pycmd int" \
+  "\\^done,result=\"42\"" \
+  "-pycmd int"
+
+mi_gdb_test "-pycmd str" \
+  "\\^done,result=\"Hello world!\"" \
+  "-pycmd str"
+
+mi_gdb_test "-pycmd ary" \
+  "\\^done,result=\\\[\"Hello\",\"42\"\\\]" \
+  "-pycmd ary"
+
+mi_gdb_test "-pycmd dct" \
+  "\\^done,result={hello=\"world\",times=\"42\"}" \
+  "-pycmd dct"
+
+mi_gdb_test "-pycmd bk1" \
+  "\\^error,msg=\"Non-string object used as key: Bad Kay.\"" \
+  "-pycmd bk1"
+
+mi_gdb_test "-pycmd bk2" \
+  "\\^error,msg=\"Non-string object used as key: 1.\"" \
+  "-pycmd bk2"
+
+mi_gdb_test "-pycmd bk3" \
+  "\\^error,msg=\"Non-string object used as key: __repr__ returned non-string .*" \
+  "-pycmd bk3"
+
+mi_gdb_test "-pycmd tpl" \
+  "\\^done,result=\\\[\"42\",\"Hello\"\\\]" \
+  "-pycmd tpl"
+
+mi_gdb_test "-pycmd itr" \
+  "\\^done,result=\\\[\"1\",\"2\",\"3\"\\\]" \
+  "-pycmd itr"
+
+mi_gdb_test "-pycmd nn1" \
+  "\\^done" \
+  "-pycmd nn1"
+
+mi_gdb_test "-pycmd nn2" \
+  "\\^done,result=\\\[\"None\"\\\]" \
+  "-pycmd nn2"
+
+mi_gdb_test "-pycmd bogus" \
+  "\\^error,msg=\"-pycmd: Invalid parameter: bogus\"" \
+  "-pycmd bogus"
+
+mi_gdb_test "-pycmd exp" \
+  "\\^error,msg=\"-pycmd: failed to execute command\"" \
+  "-pycmd exp"
+
+mi_gdb_test "python pycmd2('-pycmd')" \
+  ".*\\^done" \
+  "Redefine -pycmd MI command from CLI command"
+
+mi_gdb_test "-pycmd str" \
+  "\\^done,result=\"Ciao!\"" \
+  "-pycmd str - redefined from CLI"
+
+mi_gdb_test "-pycmd int" \
+  "\\^error,msg=\"-pycmd: Invalid parameter: int\"" \
+  "-pycmd int - redefined from CLI"
+
+mi_gdb_test "-pycmd red" \
+    "\\^error,msg=\"-pycmd: unable to add command, name may already be in use\"" \
+  "Redefine -pycmd MI command from Python MI command"
+
+mi_gdb_test "-pycmd new" \
+    "\\^done" \
+  "Define new command -pycmd-new MI command from Python MI command"
+
+mi_gdb_test "-pycmd-new int" \
+  "\\^done,result=\"42\"" \
+  "-pycmd int - redefined from MI"
+
+mi_gdb_test "python pycmd1('')" \
+  ".*\\^error,msg=\"MI command name is empty.\"" \
+  "empty MI command name"
+
+mi_gdb_test "python pycmd1('-')" \
+  ".*\\^error,msg=\"MI command name does not start with '-' followed by at least one letter or digit.\"" \
+  "invalid MI command name"
+
+mi_gdb_test "python pycmd1('-bad-character-@')" \
+  ".*\\^error,msg=\"MI command name contains invalid character: @.\"" \
+  "invalid character in MI command name"
diff --git a/gdb/testsuite/gdb.python/py-mi-cmd.py b/gdb/testsuite/gdb.python/py-mi-cmd.py
new file mode 100644
index 00000000000..fd09d8dca00
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-mi-cmd.py
@@ -0,0 +1,57 @@
+import gdb
+
+class BadKey:
+    def __repr__(self):
+        return "Bad Kay"
+
+class ReallyBadKey:
+    def __repr__(self):
+        return BadKey()
+
+
+class pycmd1(gdb.MICommand):
+    def invoke(self, argv):
+        if argv[0] == 'int':
+            return 42
+        elif argv[0] == 'str':
+            return "Hello world!"
+        elif argv[0] == 'ary':
+            return [ 'Hello', 42 ]
+        elif argv[0] == "dct":
+            return { 'hello' : 'world', 'times' : 42}
+        elif argv[0] == "bk1":
+            return { BadKey() : 'world' }
+        elif argv[0] == "bk2":
+            return { 1 : 'world' }
+        elif argv[0] == "bk3":
+            return { ReallyBadKey() : 'world' }
+        elif argv[0] == 'tpl':
+            return ( 42 , 'Hello' )
+        elif argv[0] == 'itr':
+            return iter([1,2,3])
+        elif argv[0] == 'nn1':
+            return None
+        elif argv[0] == 'nn2':
+            return [ None ]
+        elif argv[0] == 'red':
+            pycmd2('-pycmd')
+            return None
+        elif argv[0] == 'exp':
+            raise gdb.GdbError()
+        else:
+            raise gdb.GdbError("Invalid parameter: %s" % argv[0])
+
+
+class pycmd2(gdb.MICommand):
+    def invoke(self, argv):
+        if argv[0] == 'str':
+            return "Ciao!"
+        elif argv[0] == 'red':
+            pycmd1('-pycmd')
+            raise gdb.GdbError("Command redefined but we failing anyway")
+        elif argv[0] == 'new':
+            pycmd1('-pycmd-new')
+            return None
+        else:
+            raise gdb.GdbError("Invalid parameter: %s" % argv[0])
+



More information about the Gdb-patches mailing list