Implement gdb.execute_mi

This adds a new Python function, gdb.execute_mi, that can be used to
invoke an MI command but get the output as a Python object, rather
than a string.  This is done by implementing a new ui_out subclass
that builds a Python object.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=11688
Reviewed-By: Eli Zaretskii <eliz@gnu.org>
This commit is contained in:
Tom Tromey 2023-03-16 10:57:32 -06:00
parent e7a2797eb0
commit c97d123d67
10 changed files with 419 additions and 0 deletions

View file

@ -414,6 +414,7 @@ SUBDIR_PYTHON_SRCS = \
python/py-lazy-string.c \ python/py-lazy-string.c \
python/py-linetable.c \ python/py-linetable.c \
python/py-membuf.c \ python/py-membuf.c \
python/py-mi.c \
python/py-micmd.c \ python/py-micmd.c \
python/py-newobjfileevent.c \ python/py-newobjfileevent.c \
python/py-objfile.c \ python/py-objfile.c \

View file

@ -192,6 +192,9 @@ info main
- Changes are backwards compatible, the older API can still be - Changes are backwards compatible, the older API can still be
used to disassemble instructions without styling. used to disassemble instructions without styling.
** New function gdb.execute_mi(COMMAND, [ARG]...), that invokes a
GDB/MI command and returns the output as a Python dictionary.
*** Changes in GDB 13 *** Changes in GDB 13
* MI version 1 is deprecated, and will be removed in GDB 14. * MI version 1 is deprecated, and will be removed in GDB 14.

View file

@ -4584,6 +4584,36 @@ commands have been added:
(@value{GDBP}) (@value{GDBP})
@end smallexample @end smallexample
Conversely, it is possible to execute @sc{gdb/mi} commands from
Python, with the results being a Python object and not a
specially-formatted string. This is done with the
@code{gdb.execute_mi} function.
@findex gdb.execute_mi
@defun gdb.execute_mi (command @r{[}, arg @r{]}@dots{})
Invoke a @sc{gdb/mi} command. @var{command} is the name of the
command, a string. The arguments, @var{arg}, are passed to the
command. Each argument must also be a string.
This function returns a Python dictionary whose contents reflect the
corresponding @sc{GDB/MI} command's output. Refer to the
documentation for these commands for details. Lists are represented
as Python lists, and tuples are represented as Python dictionaries.
If the command fails, it will raise a Python exception.
@end defun
Here is how this works using the commands from the example above:
@smallexample
(@value{GDBP}) python print(gdb.execute_mi("-echo-dict", "abc", "def", "ghi"))
@{'dict': @{'argv': ['abc', 'def', 'ghi']@}@}
(@value{GDBP}) python print(gdb.execute_mi("-echo-list", "abc", "def", "ghi"))
@{'list': ['abc', 'def', 'ghi']@}
(@value{GDBP}) python print(gdb.execute_mi("-echo-string", "abc", "def", "ghi"))
@{'string': 'abc, def, ghi'@}
@end smallexample
@node Parameters In Python @node Parameters In Python
@subsubsection Parameters In Python @subsubsection Parameters In Python

View file

@ -206,6 +206,11 @@ extern mi_command *mi_cmd_lookup (const char *command);
extern void mi_execute_command (const char *cmd, int from_tty); extern void mi_execute_command (const char *cmd, int from_tty);
/* Execute an MI command given an already-constructed parse
object. */
extern void mi_execute_command (mi_parse *context);
/* Insert COMMAND into the global mi_cmd_table. Return false if /* Insert COMMAND into the global mi_cmd_table. Return false if
COMMAND->name already exists in mi_cmd_table, in which case COMMAND will COMMAND->name already exists in mi_cmd_table, in which case COMMAND will
not have been added to mi_cmd_table. Otherwise, return true, and not have been added to mi_cmd_table. Otherwise, return true, and

View file

@ -1965,6 +1965,21 @@ mi_execute_command (const char *cmd, int from_tty)
} }
} }
/* See mi-cmds.h. */
void
mi_execute_command (mi_parse *context)
{
if (context->op != MI_COMMAND)
error (_("Command is not an MI command"));
scoped_restore save_token = make_scoped_restore (&current_token,
context->token);
scoped_restore save_debug = make_scoped_restore (&mi_debug_p, 0);
mi_cmd_execute (context);
}
/* Captures the current user selected context state, that is the current /* Captures the current user selected context state, that is the current
thread and frame. Later we can then check if the user selected context thread and frame. Later we can then check if the user selected context
has changed at all. */ has changed at all. */

298
gdb/python/py-mi.c Normal file
View file

@ -0,0 +1,298 @@
/* Python interface to MI commands
Copyright (C) 2023 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 "defs.h"
#include "python-internal.h"
#include "ui-out.h"
#include "mi/mi-parse.h"
/* A ui_out subclass that creates a Python object based on the data
that is passed in. */
class py_ui_out : public ui_out
{
public:
py_ui_out ()
: ui_out (fix_multi_location_breakpoint_output
| fix_breakpoint_script_output)
{
do_begin (ui_out_type_tuple, nullptr);
}
bool can_emit_style_escape () const override
{ return false; }
bool do_is_mi_like_p () const override
{ return true; }
/* Return the Python object that was created. If a Python error
occurred during the processing, set the Python error and return
nullptr. */
PyObject *result ()
{
if (m_error.has_value ())
{
m_error->restore ();
return nullptr;
}
return current ().obj.release ();
}
protected:
void do_progress_end () override { }
void do_progress_start () override { }
void do_progress_notify (const std::string &, const char *, double, double)
override
{ }
void do_table_begin (int nbrofcols, int nr_rows, const char *tblid) override
{
do_begin (ui_out_type_list, tblid);
}
void do_table_body () override
{ }
void do_table_end () override
{
do_end (ui_out_type_list);
}
void do_table_header (int width, ui_align align,
const std::string &col_name,
const std::string &col_hdr) override
{ }
void do_begin (ui_out_type type, const char *id) override;
void do_end (ui_out_type type) override;
void do_field_signed (int fldno, int width, ui_align align,
const char *fldname, LONGEST value) override;
void do_field_unsigned (int fldno, int width, ui_align align,
const char *fldname, ULONGEST value) override;
void do_field_skip (int fldno, int width, ui_align align,
const char *fldname) override
{ }
void do_field_string (int fldno, int width, ui_align align,
const char *fldname, const char *string,
const ui_file_style &style) override;
void do_field_fmt (int fldno, int width, ui_align align,
const char *fldname, const ui_file_style &style,
const char *format, va_list args) override
ATTRIBUTE_PRINTF (7, 0);
void do_spaces (int numspaces) override
{ }
void do_text (const char *string) override
{ }
void do_message (const ui_file_style &style,
const char *format, va_list args)
override ATTRIBUTE_PRINTF (3,0)
{ }
void do_wrap_hint (int indent) override
{ }
void do_flush () override
{ }
void do_redirect (struct ui_file *outstream) override
{ }
private:
/* When constructing Python objects, this class keeps a stack of
objects being constructed. Each such object has this type. */
struct object_desc
{
/* Name of the field (or empty for lists) that this object will
eventually become. */
std::string field_name;
/* The object under construction. */
gdbpy_ref<> obj;
/* The type of structure being created. Note that tables are
treated as lists here. */
ui_out_type type;
};
/* The stack of objects being created. */
std::vector<object_desc> m_objects;
/* If an error occurred, this holds the exception information for
use by the 'release' method. */
gdb::optional<gdbpy_err_fetch> m_error;
/* Return a reference to the object under construction. */
object_desc &current ()
{ return m_objects.back (); }
/* Add a new field to the current object under construction. */
void add_field (const char *name, const gdbpy_ref<> &obj);
};
void
py_ui_out::add_field (const char *name, const gdbpy_ref<> &obj)
{
if (obj == nullptr)
{
m_error.emplace ();
return;
}
object_desc &desc = current ();
if (desc.type == ui_out_type_list)
{
if (PyList_Append (desc.obj.get (), obj.get ()) < 0)
m_error.emplace ();
}
else
{
if (PyDict_SetItemString (desc.obj.get (), name, obj.get ()) < 0)
m_error.emplace ();
}
}
void
py_ui_out::do_begin (ui_out_type type, const char *id)
{
if (m_error.has_value ())
return;
gdbpy_ref<> new_obj (type == ui_out_type_list
? PyList_New (0)
: PyDict_New ());
if (new_obj == nullptr)
{
m_error.emplace ();
return;
}
object_desc new_desc;
if (id != nullptr)
new_desc.field_name = id;
new_desc.obj = std::move (new_obj);
new_desc.type = type;
m_objects.push_back (std::move (new_desc));
}
void
py_ui_out::do_end (ui_out_type type)
{
if (m_error.has_value ())
return;
object_desc new_obj = std::move (current ());
m_objects.pop_back ();
add_field (new_obj.field_name.c_str (), new_obj.obj);
}
void
py_ui_out::do_field_signed (int fldno, int width, ui_align align,
const char *fldname, LONGEST value)
{
if (m_error.has_value ())
return;
gdbpy_ref<> val = gdb_py_object_from_longest (value);
add_field (fldname, val);
}
void
py_ui_out::do_field_unsigned (int fldno, int width, ui_align align,
const char *fldname, ULONGEST value)
{
if (m_error.has_value ())
return;
gdbpy_ref<> val = gdb_py_object_from_ulongest (value);
add_field (fldname, val);
}
void
py_ui_out::do_field_string (int fldno, int width, ui_align align,
const char *fldname, const char *string,
const ui_file_style &style)
{
if (m_error.has_value ())
return;
gdbpy_ref<> val = host_string_to_python_string (string);
add_field (fldname, val);
}
void
py_ui_out::do_field_fmt (int fldno, int width, ui_align align,
const char *fldname, const ui_file_style &style,
const char *format, va_list args)
{
if (m_error.has_value ())
return;
std::string str = string_vprintf (format, args);
do_field_string (fldno, width, align, fldname, str.c_str (), style);
}
/* Implementation of the gdb.execute_mi command. */
PyObject *
gdbpy_execute_mi_command (PyObject *self, PyObject *args, PyObject *kw)
{
gdb::unique_xmalloc_ptr<char> mi_command;
std::vector<gdb::unique_xmalloc_ptr<char>> arg_strings;
Py_ssize_t n_args = PyTuple_Size (args);
if (n_args < 0)
return nullptr;
for (Py_ssize_t i = 0; i < n_args; ++i)
{
/* Note this returns a borrowed reference. */
PyObject *arg = PyTuple_GetItem (args, i);
if (arg == nullptr)
return nullptr;
gdb::unique_xmalloc_ptr<char> str = python_string_to_host_string (arg);
if (str == nullptr)
return nullptr;
if (i == 0)
mi_command = std::move (str);
else
arg_strings.push_back (std::move (str));
}
py_ui_out uiout;
try
{
scoped_restore save_uiout = make_scoped_restore (&current_uiout, &uiout);
std::unique_ptr<struct mi_parse> parser
= mi_parse::make (std::move (mi_command), std::move (arg_strings));
mi_execute_command (parser.get ());
}
catch (const gdb_exception &except)
{
gdbpy_convert_exception (except);
return nullptr;
}
return uiout.result ();
}

View file

@ -483,6 +483,9 @@ struct symtab_and_line *sal_object_to_symtab_and_line (PyObject *obj);
frame_info_ptr frame_object_to_frame_info (PyObject *frame_obj); frame_info_ptr frame_object_to_frame_info (PyObject *frame_obj);
struct gdbarch *arch_object_to_gdbarch (PyObject *obj); struct gdbarch *arch_object_to_gdbarch (PyObject *obj);
extern PyObject *gdbpy_execute_mi_command (PyObject *self, PyObject *args,
PyObject *kw);
/* Convert Python object OBJ to a program_space pointer. OBJ must be a /* Convert Python object OBJ to a program_space pointer. OBJ must be a
gdb.Progspace reference. Return nullptr if the gdb.Progspace is not gdb.Progspace reference. Return nullptr if the gdb.Progspace is not
valid (see gdb.Progspace.is_valid), otherwise return the program_space valid (see gdb.Progspace.is_valid), otherwise return the program_space

View file

@ -2484,6 +2484,11 @@ PyMethodDef python_GdbMethods[] =
Evaluate command, a string, as a gdb CLI command. Optionally returns\n\ Evaluate command, a string, as a gdb CLI command. Optionally returns\n\
a Python String containing the output of the command if to_string is\n\ a Python String containing the output of the command if to_string is\n\
set to True." }, set to True." },
{ "execute_mi", (PyCFunction) gdbpy_execute_mi_command,
METH_VARARGS | METH_KEYWORDS,
"execute_mi (command, arg...) -> dictionary\n\
Evaluate command, a string, as a gdb MI command.\n\
Arguments (also strings) are passed to the command." },
{ "parameter", gdbpy_parameter, METH_VARARGS, { "parameter", gdbpy_parameter, METH_VARARGS,
"Return a gdb parameter's value" }, "Return a gdb parameter's value" },

View file

@ -0,0 +1,32 @@
# Copyright (C) 2023 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 gdb.execute_mi.
load_lib gdb-python.exp
require allow_python_tests
clean_restart
gdb_test_no_output "source ${srcdir}/${subdir}/py-mi-cmd.py" \
"load python file"
gdb_test "python run_execute_mi_tests()" "PASS"
# Be sure to test a command implemented as CLI command, as those fetch
# the args.
gdb_test_no_output "python gdb.execute_mi('-exec-arguments', 'a', 'b', 'c')" \
"set arguments"
gdb_test "show args" ".*\"a b c\"."

View file

@ -118,3 +118,30 @@ def free_invoke(obj, args):
# these as a Python function which is then called from the exp script. # these as a Python function which is then called from the exp script.
def run_exception_tests(): def run_exception_tests():
print("PASS") print("PASS")
# Run some execute_mi tests. This is easier to do from Python.
def run_execute_mi_tests():
# Install the command.
cmd = pycmd1("-pycmd")
# Pass in a representative subset of the pycmd1 keys, and then
# check that the result via MI is the same as the result via a
# direct Python call. Note that some results won't compare as
# equal -- for example, a Python MI command can return a tuple,
# but that will be translated to a Python list.
for name in ("int", "str", "dct"):
expect = cmd.invoke([name])
got = gdb.execute_mi("-pycmd", name)
if expect != got:
print("FAIL: saw " + repr(got) + ", but expected " + repr(expect))
return
ok = False
try:
gdb.execute_mi("-pycmd", "exp")
# Due to the "denaturation" problem, we have to expect a gdb.error
# here and not a gdb.GdbError.
except gdb.error:
ok = True
if not ok:
print("FAIL: did not throw exception")
print("PASS")