[v2,3/4] Add wrappers for Python implementation functions and methods

Message ID 20260515-python-safety-initial-v2-3-6129cadf258a@tromey.com
State New
Headers
Series Python safety initial work |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 fail Patch failed to apply
linaro-tcwg-bot/tcwg_gdb_check--master-arm fail Patch failed to apply

Commit Message

Tom Tromey May 15, 2026, 7:59 p.m. UTC
  This adds some wrappers for Python implementation functions and
methods, and a couple of new constexpr functions to create PyMethodDef
entries.  This provides a few safety benefits:

* The new-style API approach (see previous patch) is implemented by
  the wrapper.  That is, exceptions are caught here and transformed.

* The implementation functions can now return any reasonable type,
  with automatic conversion by the wrapper.

* The function API and the appropriate METH_* flags are handled
  together, avoiding any possible discrepancy.

This approach also means that we can modify the old rule that gdb
calls must be wrapped in a try/catch -- the try/catch is now provided
by the wrapper function, so the implementation can be written in a
more natural way.

Note this patch is not 100% complete.  There should be one more
wrapper for case where a method takes a single argument (though we
probably cannot use METH_O unfortunately).  There may be some other
holes as well.
---
 gdb/python/py-safety.h       | 320 +++++++++++++++++++++++++++++++++++++++++++
 gdb/python/python-internal.h |   1 +
 2 files changed, 321 insertions(+)
  

Comments

Andrew Burgess May 18, 2026, 9:33 a.m. UTC | #1
Tom Tromey <tom@tromey.com> writes:

> This adds some wrappers for Python implementation functions and
> methods, and a couple of new constexpr functions to create PyMethodDef
> entries.  This provides a few safety benefits:
>
> * The new-style API approach (see previous patch) is implemented by
>   the wrapper.  That is, exceptions are caught here and transformed.
>
> * The implementation functions can now return any reasonable type,
>   with automatic conversion by the wrapper.
>
> * The function API and the appropriate METH_* flags are handled
>   together, avoiding any possible discrepancy.
>
> This approach also means that we can modify the old rule that gdb
> calls must be wrapped in a try/catch -- the try/catch is now provided
> by the wrapper function, so the implementation can be written in a
> more natural way.
>
> Note this patch is not 100% complete.  There should be one more
> wrapper for case where a method takes a single argument (though we
> probably cannot use METH_O unfortunately).  There may be some other
> holes as well.
> ---
>  gdb/python/py-safety.h       | 320 +++++++++++++++++++++++++++++++++++++++++++
>  gdb/python/python-internal.h |   1 +
>  2 files changed, 321 insertions(+)
>
> diff --git a/gdb/python/py-safety.h b/gdb/python/py-safety.h
> new file mode 100644
> index 00000000000..62018dacc0f
> --- /dev/null
> +++ b/gdb/python/py-safety.h
> @@ -0,0 +1,320 @@
> +/* Wrappers for some Python safety.
> +
> +   Copyright (C) 2026 Free Software Foundation, Inc.
> +
> +   This file is part of GDB.
> +
> +   This program is free software; you can redistribute it and/or modify
> +   it under the terms of the GNU General Public License as published by
> +   the Free Software Foundation; either version 3 of the License, or
> +   (at your option) any later version.
> +
> +   This program is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +   GNU General Public License for more details.
> +
> +   You should have received a copy of the GNU General Public License
> +   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
> +
> +#ifndef GDB_PYTHON_PY_SAFETY_H
> +#define GDB_PYTHON_PY_SAFETY_H
> +
> +#include "gdbsupport/traits.h"
> +#include "py-ref.h"
> +#include "py-wrappers.h"
> +#include "charset.h"
> +
> +/* This file holds wrapper templates for the various ways that gdb
> +   code might be exposed to Python.  These wrappers are part of gdb's
> +   "Python safety" approach -- utilities designed to try to prevent
> +   refcount problems, missing error checks, and that also remove the
> +   need to wrap calls into gdb in an explicit try/catch.
> +
> +   See py-wrappers.h for some more discussion of this.
> +
> +   Implementation functions and methods -- the stuff you write to
> +   expose some part of gdb to Python -- are written in a certain
> +   style.
> +
> +   An implementation function is a module-level function.  For example
> +   something like 'gdb.register_window_type' is implemented as a
> +   function.  An implementation method is a method of the gdb class
> +   implementing a certain Python type; for example
> +   'gdb.TuiWindow.width' is implemented via a method of
> +   gdbpy_tui_window.
> +
> +   Each of these will accept gdbpy_borrowed_ref arguments (or in some
> +   more limited situations, a gdbpy_opt_borrowed_ref) and return any
> +   relevant type, which will be automatically converted (see the
> +   'to_python' overloads below) to the correct Python type.  Note that
> +   if the 'to_python' functions aren't appropriate in your case, you
> +   can always use the escape hatch of returning a gdbpy_ref<> and
> +   handling type conversion manually.
> +
> +   There are some constexpr functions below that wrap the
> +   implementation methods, and create PyMethodDef objects and the
> +   like.
> +
> +   Implementation methods are expected to use the wrappers in
> +   py-wrappers.h and not generally call into Python directly.
> +
> +   Implementation details of the method-wrapping safety code are put
> +   into this namespace, just to emphasize that these shouldn't be used
> +   elsewhere.  Skip past the namespace to find the public APIs.  */
> +namespace safety_details
> +{
> +/* Overloads of "to_python" are used by the safety wrappers to convert
> +   a function's real return value to a Python object.  A new reference
> +   will always be returned.  For the time being these are kept as a
> +   detail of the method-wrapping code.  However we may want to
> +   consider exposing these more generally.  */
> +
> +static inline PyObject *
> +to_python (bool value)
> +{
> +  /* Note that this cannot fail.  */
> +  return PyBool_FromLong (value);
> +}
> +
> +template<typename T, typename = std::is_integral<T>>
> +static inline PyObject *
> +to_python (T value)
> +{
> +  if constexpr (std::is_signed<T>::value)
> +    return gdb_py_object_from_longest (value).release ();
> +  else
> +    return gdb_py_object_from_ulongest (value).release ();
> +}
> +
> +static inline PyObject *
> +to_python (const char *value)
> +{
> +  if (value == nullptr)
> +    return py_none ().release ();
> +  return PyUnicode_Decode (value, strlen (value), host_charset (), nullptr);
> +}
> +
> +static inline PyObject *
> +to_python (std::string &&value)
> +{
> +  return PyUnicode_Decode (value.c_str (), value.size (),
> +			   host_charset (), nullptr);
> +}
> +
> +static inline PyObject *
> +to_python (const std::string &value)
> +{
> +  return PyUnicode_Decode (value.c_str (), value.size (),
> +			   host_charset (), nullptr);
> +}
> +
> +static inline PyObject *
> +to_python (gdb::unique_xmalloc_ptr<char> &&value)
> +{
> +  if (value == nullptr)
> +    return py_none ().release ();
> +  return PyUnicode_Decode (value.get (), strlen (value.get ()),
> +			   host_charset (), nullptr);
> +}
> +
> +static inline PyObject *
> +to_python (gdbpy_ref<> &&value)
> +{
> +  return value.release ();
> +}
> +
> +/* An instantiation of this function is used when calling a gdb method
> +   from Python.  It accepts some number of arguments (normally
> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
> +   the underlying function F.  Any exceptions are caught and
> +   converted, and the return value of F is converted to a Python
> +   object as appropriate.  */
> +template<auto F, typename... Args>
> +PyObject *
> +wrapped_function (Args... args)
> +{
> +  try
> +    {
> +      using result_type = typename std::invoke_result_t<decltype (F), Args...>;
> +
> +      if constexpr (std::is_void_v<result_type>)
> +	{
> +	  F (args...);
> +	  return py_none ().release ();
> +	}
> +      else
> +	return to_python (F (args...));
> +    }
> +  catch (const gdb_python_exception &pye)
> +    {
> +      gdb_assert (PyErr_Occurred () != nullptr);
> +      return nullptr;
> +    }
> +  catch (const gdb_exception &exc)
> +    {
> +      return gdbpy_handle_gdb_exception (nullptr, exc);
> +    }
> +}
> +
> +/* An instantiation of this function is used when calling a gdb method
> +   from Python.  It accepts some number of arguments (normally
> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
> +   the underlying function F.  Any exceptions are caught and
> +   converted, and the return value of F is converted to a Python
> +   object as appropriate.  */
> +template<typename Class, typename Ret, typename... Args>
> +PyObject *
> +wrapped_method (Ret (Class::*meth) (Args...), Class *self, Args... args)
> +{
> +  try
> +    {
> +      if constexpr (std::is_void_v<Ret>)
> +	{
> +	  (self->*meth) (args...);
> +	  return py_none ().release ();
> +	}
> +      else
> +	return to_python ((self->*meth) (args...));
> +    }
> +  catch (const gdb_python_exception &pye)
> +    {
> +      gdb_assert (PyErr_Occurred () != nullptr);
> +      return nullptr;
> +    }
> +  catch (const gdb_exception &exc)
> +    {
> +      return gdbpy_handle_gdb_exception (nullptr, exc);
> +    }
> +}
> +
> +template<auto F>
> +PyObject *
> +fn_wrapper (PyObject *self, PyObject *args, PyObject *kw)
> +{
> +  return wrapped_function<F> (gdbpy_borrowed_ref (args),
> +			      gdbpy_opt_borrowed_ref (kw));
> +}
> +
> +template<typename C, auto M>
> +PyObject *
> +varargs_wrapper (PyObject *self, PyObject *args, PyObject *kw)
> +{
> +  return wrapped_method (M, static_cast<C *> (self),
> +			 gdbpy_borrowed_ref (args),
> +			 gdbpy_opt_borrowed_ref (kw));
> +}
> +
> +} /* namespace safety_details */
> +
> +/* Create a PyMethodDef for a no-argument method.  It takes the
> +   underlying class C and a pointer-to-method M as template
> +   parameters, and the name and documentation as arguments.  The
> +   method M is wrapped to call to_python and to catch exceptions per
> +   the safety protocol.  */
> +template<typename C, auto M>
> +constexpr PyMethodDef
> +noargs_method (std::string_view name, std::string_view doc)

Using std::string_view seems a little suspect here.  Calling
std::string_view::data doesn't guarantee a NULL terminated string, and
creating a string_view from a C-string only encodes the length of the
C-string, not 'length + 1' which would be required to capture the NULL
terminator.

Now given how this gets used in the next patch we're going to be fine,
you pass a C-string, Python will read outside the bounds of the
string_view and find the NULL terminator, but that all just seems a
little ... iffy.

If we don't actually care about the length of the string_view, then
should we be using string_view at all?  Shouldn't we just be honest with
ourselves and pass through 'const char *'?

There are other uses of string_view below, and the same thoughts apply.

> +{
> +  using namespace safety_details;
> +  return {
> +    name.data (),
> +    [] (PyObject *self, PyObject *args) -> PyObject *
> +    {
> +      return wrapped_method (M, static_cast<C *> (self));
> +    },
> +    METH_NOARGS,
> +    doc.data (),
> +  };
> +}
> +
> +/* This is used to create the PyMethodDef for a varargs function.  It
> +   takes the underlying implementation function as a template
> +   argument, and also arguments for the method name and documentation
> +   string.
> +
> +   The underlying function should accept a gdbpy_borrowed_ref argument
> +   (the arguments to the Python function), and then a
> +   gdbpy_opt_borrowed_ref for the keywords.  The function can return
> +   any type (see the to_python overloads); and should throw an
> +   exception on error.  If gdb_python_exception is thrown, the Python
> +   exception must already have been set.
> +
> +   The gdb policy is that varargs methods must also accept keywords,
> +   and this is enforced here.  */
> +template<auto F>
> +constexpr PyMethodDef
> +varargs_function (std::string_view name, std::string_view doc)
> +{
> +  using namespace safety_details;
> +  return {
> +    name.data (),
> +    (PyCFunction) fn_wrapper<F>,
> +    /* gdb's rule is that varargs should also use keywords.  */
> +    METH_VARARGS | METH_KEYWORDS,
> +    doc.data (),
> +  };
> +}
> +
> +/* Like a varargs function, but this implements a method on some
> +   Python type that gdb provides.  The implementation class and a
> +   pointer-to-method must be specified.  */
> +template<typename C, auto M>
> +constexpr PyMethodDef
> +varargs_method (std::string_view name, std::string_view doc)
> +{
> +  using namespace safety_details;
> +  return {
> +    name.data (),
> +    (PyCFunction) varargs_wrapper<C, M>,
> +    /* gdb's rule is that varargs should also use keywords.  */
> +    METH_VARARGS | METH_KEYWORDS,
> +    doc.data (),
> +  };
> +}
> +
> +/* A function that wraps a "repr" or "str" method.  */
> +template<typename C, auto M>
> +PyObject *
> +wrap_repr (PyObject *arg)

Maybe something like 'wrap_tp_callback' might be better?  I just know
every time I see 'wrap_repr' for something that isn't 'repr' I'm going
to assume copy & paste error, and then have to go and remind myself that
'wrap_repr' has wider uses.  Now's a great time to find a better name
before this starts getting used more widely..

Thanks,
Andrew

> +{
> +  using namespace safety_details;
> +  return wrapped_method (M, static_cast<C *> (arg));
> +}
> +
> +/* A function that wraps a "get" method.  */
> +template<typename C, auto M>
> +PyObject *
> +wrap_getter (PyObject *arg, void *closure)
> +{
> +  using namespace safety_details;
> +  /* In gdb the closure argument is never used.  */
> +  return wrapped_method (M, static_cast<C *> (arg));
> +}
> +
> +/* A function that wraps a "set" method.  */
> +template<typename C, void (C::*M) (gdbpy_opt_borrowed_ref)>
> +int
> +wrap_setter (PyObject *arg, PyObject *value, void *closure)
> +{
> +  using namespace safety_details;
> +  try
> +    {
> +      C *self = static_cast<C *> (arg);
> +      /* In gdb the closure argument is never used.  */
> +      (self->*M) (gdbpy_opt_borrowed_ref (value));
> +    }
> +  catch (const gdb_python_exception &pye)
> +    {
> +      gdb_assert (PyErr_Occurred () != nullptr);
> +      return -1;
> +    }
> +  catch (const gdb_exception &exc)
> +    {
> +      return gdbpy_handle_gdb_exception (-1, exc);
> +    }
> +
> +  return 0;
> +}
> +
> +#endif /* GDB_PYTHON_PY_SAFETY_H */
> diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
> index 6df0c62e2b3..e4f35f8cd88 100644
> --- a/gdb/python/python-internal.h
> +++ b/gdb/python/python-internal.h
> @@ -1383,5 +1383,6 @@ py_notimplemented ()
>  #undef Py_RETURN_NOTIMPLEMENTED
>  
>  #include "py-wrappers.h"
> +#include "py-safety.h"
>  
>  #endif /* GDB_PYTHON_PYTHON_INTERNAL_H */
>
> -- 
> 2.49.0
  
Andrew Burgess May 18, 2026, 10 a.m. UTC | #2
Tom Tromey <tom@tromey.com> writes:

> This adds some wrappers for Python implementation functions and
> methods, and a couple of new constexpr functions to create PyMethodDef
> entries.  This provides a few safety benefits:
>
> * The new-style API approach (see previous patch) is implemented by
>   the wrapper.  That is, exceptions are caught here and transformed.
>
> * The implementation functions can now return any reasonable type,
>   with automatic conversion by the wrapper.
>
> * The function API and the appropriate METH_* flags are handled
>   together, avoiding any possible discrepancy.
>
> This approach also means that we can modify the old rule that gdb
> calls must be wrapped in a try/catch -- the try/catch is now provided
> by the wrapper function, so the implementation can be written in a
> more natural way.
>
> Note this patch is not 100% complete.  There should be one more
> wrapper for case where a method takes a single argument (though we
> probably cannot use METH_O unfortunately).  There may be some other
> holes as well.

Maybe reword this so it doesn't imply that THIS patch is not complete,
but rather the implementation as a whole is not complete and will
require future follow on patches.  What you've got is good enough to
start using it.


> ---
>  gdb/python/py-safety.h       | 320 +++++++++++++++++++++++++++++++++++++++++++
>  gdb/python/python-internal.h |   1 +
>  2 files changed, 321 insertions(+)
>
> diff --git a/gdb/python/py-safety.h b/gdb/python/py-safety.h
> new file mode 100644
> index 00000000000..62018dacc0f
> --- /dev/null
> +++ b/gdb/python/py-safety.h
> @@ -0,0 +1,320 @@
> +/* Wrappers for some Python safety.
> +
> +   Copyright (C) 2026 Free Software Foundation, Inc.
> +
> +   This file is part of GDB.
> +
> +   This program is free software; you can redistribute it and/or modify
> +   it under the terms of the GNU General Public License as published by
> +   the Free Software Foundation; either version 3 of the License, or
> +   (at your option) any later version.
> +
> +   This program is distributed in the hope that it will be useful,
> +   but WITHOUT ANY WARRANTY; without even the implied warranty of
> +   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +   GNU General Public License for more details.
> +
> +   You should have received a copy of the GNU General Public License
> +   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
> +
> +#ifndef GDB_PYTHON_PY_SAFETY_H
> +#define GDB_PYTHON_PY_SAFETY_H
> +
> +#include "gdbsupport/traits.h"
> +#include "py-ref.h"
> +#include "py-wrappers.h"
> +#include "charset.h"
> +
> +/* This file holds wrapper templates for the various ways that gdb
> +   code might be exposed to Python.  These wrappers are part of gdb's
> +   "Python safety" approach -- utilities designed to try to prevent
> +   refcount problems, missing error checks, and that also remove the
> +   need to wrap calls into gdb in an explicit try/catch.
> +
> +   See py-wrappers.h for some more discussion of this.
> +
> +   Implementation functions and methods -- the stuff you write to
> +   expose some part of gdb to Python -- are written in a certain
> +   style.
> +
> +   An implementation function is a module-level function.  For example
> +   something like 'gdb.register_window_type' is implemented as a
> +   function.  An implementation method is a method of the gdb class
> +   implementing a certain Python type; for example
> +   'gdb.TuiWindow.width' is implemented via a method of
> +   gdbpy_tui_window.
> +
> +   Each of these will accept gdbpy_borrowed_ref arguments (or in some
> +   more limited situations, a gdbpy_opt_borrowed_ref) and return any
> +   relevant type, which will be automatically converted (see the
> +   'to_python' overloads below) to the correct Python type.  Note that
> +   if the 'to_python' functions aren't appropriate in your case, you
> +   can always use the escape hatch of returning a gdbpy_ref<> and
> +   handling type conversion manually.
> +
> +   There are some constexpr functions below that wrap the
> +   implementation methods, and create PyMethodDef objects and the
> +   like.
> +
> +   Implementation methods are expected to use the wrappers in
> +   py-wrappers.h and not generally call into Python directly.
> +
> +   Implementation details of the method-wrapping safety code are put
> +   into this namespace, just to emphasize that these shouldn't be used
> +   elsewhere.  Skip past the namespace to find the public APIs.  */
> +namespace safety_details
> +{
> +/* Overloads of "to_python" are used by the safety wrappers to convert
> +   a function's real return value to a Python object.  A new reference
> +   will always be returned.  For the time being these are kept as a
> +   detail of the method-wrapping code.  However we may want to
> +   consider exposing these more generally.  */
> +
> +static inline PyObject *
> +to_python (bool value)
> +{
> +  /* Note that this cannot fail.  */
> +  return PyBool_FromLong (value);
> +}
> +
> +template<typename T, typename = std::is_integral<T>>

I think the SFINAE part here is wrong, like in the previous commit.  I
think gdb::Requires<std::is_integral<T>> might be what you mean.

> +static inline PyObject *
> +to_python (T value)
> +{
> +  if constexpr (std::is_signed<T>::value)
> +    return gdb_py_object_from_longest (value).release ();
> +  else
> +    return gdb_py_object_from_ulongest (value).release ();
> +}
> +
> +static inline PyObject *
> +to_python (const char *value)
> +{
> +  if (value == nullptr)
> +    return py_none ().release ();
> +  return PyUnicode_Decode (value, strlen (value), host_charset (), nullptr);
> +}
> +
> +static inline PyObject *
> +to_python (std::string &&value)
> +{
> +  return PyUnicode_Decode (value.c_str (), value.size (),
> +			   host_charset (), nullptr);
> +}
> +
> +static inline PyObject *
> +to_python (const std::string &value)
> +{
> +  return PyUnicode_Decode (value.c_str (), value.size (),
> +			   host_charset (), nullptr);
> +}
> +
> +static inline PyObject *
> +to_python (gdb::unique_xmalloc_ptr<char> &&value)
> +{
> +  if (value == nullptr)
> +    return py_none ().release ();
> +  return PyUnicode_Decode (value.get (), strlen (value.get ()),
> +			   host_charset (), nullptr);
> +}

Sorry for the double review.

None of the to_python functions check their return values for error, for
example PyUnicode_Decode can fail and return NULL, but you don't check
for this.

But this is OK.  to_python is only used at the point where we transition
back from GDB's C++ code to the Python internals, so if PyUnicode_Decode
(for example) returns NULL and sets an exception, this will be caught by
Python.

I have two pieces of feedback on this:

 1. I think this should be explicitly called out in the comment above
    the to_python functions, rather than making everyone figure out that
    this is not a mistake.

 2. The comment in this to_python function:

    > +static inline PyObject *
    > +to_python (bool value)
    > +{
    > +  /* Note that this cannot fail.  */
    > +  return PyBool_FromLong (value);
    > +}
    > +

    Is (IMHO) confusing.  It implies the lack of error checking here is
    because PyBool_FromLong cannot fail, which is why, when I look at
    later to_python functions which include calls that *can* fail, I
    asked myself, where's the error checking.

    I think this comment should just go.

> +
> +static inline PyObject *
> +to_python (gdbpy_ref<> &&value)
> +{
> +  return value.release ();
> +}
> +
> +/* An instantiation of this function is used when calling a gdb method
> +   from Python.  It accepts some number of arguments (normally
> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls

typo: ".... then THEN calls ..."

> +   the underlying function F.  Any exceptions are caught and
> +   converted, and the return value of F is converted to a Python
> +   object as appropriate.  */
> +template<auto F, typename... Args>
> +PyObject *
> +wrapped_function (Args... args)
> +{
> +  try
> +    {
> +      using result_type = typename std::invoke_result_t<decltype (F), Args...>;
> +
> +      if constexpr (std::is_void_v<result_type>)
> +	{
> +	  F (args...);
> +	  return py_none ().release ();
> +	}
> +      else
> +	return to_python (F (args...));
> +    }
> +  catch (const gdb_python_exception &pye)
> +    {
> +      gdb_assert (PyErr_Occurred () != nullptr);
> +      return nullptr;
> +    }
> +  catch (const gdb_exception &exc)
> +    {
> +      return gdbpy_handle_gdb_exception (nullptr, exc);
> +    }
> +}
> +
> +/* An instantiation of this function is used when calling a gdb method
> +   from Python.  It accepts some number of arguments (normally
> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
> +   the underlying function F.  Any exceptions are caught and
> +   converted, and the return value of F is converted to a Python
> +   object as appropriate.  */

This comment needs updating.  It also include the 'then then' typo from
the wrapped_function comment, but also references function F, when it
should be taking about method CLASS::METH or maybe just METH?  Anyway,
certainly not F.

> +template<typename Class, typename Ret, typename... Args>
> +PyObject *
> +wrapped_method (Ret (Class::*meth) (Args...), Class *self, Args... args)
> +{
> +  try
> +    {
> +      if constexpr (std::is_void_v<Ret>)
> +	{
> +	  (self->*meth) (args...);
> +	  return py_none ().release ();
> +	}
> +      else
> +	return to_python ((self->*meth) (args...));
> +    }
> +  catch (const gdb_python_exception &pye)
> +    {
> +      gdb_assert (PyErr_Occurred () != nullptr);
> +      return nullptr;
> +    }
> +  catch (const gdb_exception &exc)
> +    {
> +      return gdbpy_handle_gdb_exception (nullptr, exc);
> +    }
> +}
> +
> +template<auto F>
> +PyObject *
> +fn_wrapper (PyObject *self, PyObject *args, PyObject *kw)
> +{
> +  return wrapped_function<F> (gdbpy_borrowed_ref (args),
> +			      gdbpy_opt_borrowed_ref (kw));
> +}
> +
> +template<typename C, auto M>
> +PyObject *
> +varargs_wrapper (PyObject *self, PyObject *args, PyObject *kw)
> +{
> +  return wrapped_method (M, static_cast<C *> (self),
> +			 gdbpy_borrowed_ref (args),
> +			 gdbpy_opt_borrowed_ref (kw));
> +}
> +
> +} /* namespace safety_details */
> +
> +/* Create a PyMethodDef for a no-argument method.  It takes the
> +   underlying class C and a pointer-to-method M as template
> +   parameters, and the name and documentation as arguments.  The
> +   method M is wrapped to call to_python and to catch exceptions per
> +   the safety protocol.  */
> +template<typename C, auto M>
> +constexpr PyMethodDef
> +noargs_method (std::string_view name, std::string_view doc)
> +{
> +  using namespace safety_details;
> +  return {
> +    name.data (),
> +    [] (PyObject *self, PyObject *args) -> PyObject *
> +    {
> +      return wrapped_method (M, static_cast<C *> (self));
> +    },
> +    METH_NOARGS,
> +    doc.data (),
> +  };
> +}
> +
> +/* This is used to create the PyMethodDef for a varargs function.  It
> +   takes the underlying implementation function as a template
> +   argument, and also arguments for the method name and documentation
> +   string.
> +
> +   The underlying function should accept a gdbpy_borrowed_ref argument
> +   (the arguments to the Python function), and then a
> +   gdbpy_opt_borrowed_ref for the keywords.  The function can return
> +   any type (see the to_python overloads); and should throw an
> +   exception on error.  If gdb_python_exception is thrown, the Python
> +   exception must already have been set.
> +
> +   The gdb policy is that varargs methods must also accept keywords,
> +   and this is enforced here.  */
> +template<auto F>
> +constexpr PyMethodDef
> +varargs_function (std::string_view name, std::string_view doc)
> +{
> +  using namespace safety_details;
> +  return {
> +    name.data (),
> +    (PyCFunction) fn_wrapper<F>,
> +    /* gdb's rule is that varargs should also use keywords.  */
> +    METH_VARARGS | METH_KEYWORDS,
> +    doc.data (),
> +  };
> +}
> +
> +/* Like a varargs function, but this implements a method on some
> +   Python type that gdb provides.  The implementation class and a
> +   pointer-to-method must be specified.  */
> +template<typename C, auto M>
> +constexpr PyMethodDef
> +varargs_method (std::string_view name, std::string_view doc)
> +{
> +  using namespace safety_details;
> +  return {
> +    name.data (),
> +    (PyCFunction) varargs_wrapper<C, M>,
> +    /* gdb's rule is that varargs should also use keywords.  */
> +    METH_VARARGS | METH_KEYWORDS,
> +    doc.data (),
> +  };
> +}
> +
> +/* A function that wraps a "repr" or "str" method.  */
> +template<typename C, auto M>

In wrap_setter below you are explicit about the signature of M.  I much
prefer the explicit form, but here in wrap_repr and in wrap_getter you
use 'auto'.  Could we switch to the explicit form in these two too?

I know this is partly my personally preference, but I prefer to reserve
'auto' for places where either the type is unknown (e.g. lambda
functions) or for templates where the type can be different in different
instantiations.

Thanks,
Andrew

> +PyObject *
> +wrap_repr (PyObject *arg)
> +{
> +  using namespace safety_details;
> +  return wrapped_method (M, static_cast<C *> (arg));
> +}
> +
> +/* A function that wraps a "get" method.  */
> +template<typename C, auto M>
> +PyObject *
> +wrap_getter (PyObject *arg, void *closure)
> +{
> +  using namespace safety_details;
> +  /* In gdb the closure argument is never used.  */
> +  return wrapped_method (M, static_cast<C *> (arg));
> +}
> +
> +/* A function that wraps a "set" method.  */
> +template<typename C, void (C::*M) (gdbpy_opt_borrowed_ref)>
> +int
> +wrap_setter (PyObject *arg, PyObject *value, void *closure)
> +{
> +  using namespace safety_details;
> +  try
> +    {
> +      C *self = static_cast<C *> (arg);
> +      /* In gdb the closure argument is never used.  */
> +      (self->*M) (gdbpy_opt_borrowed_ref (value));
> +    }
> +  catch (const gdb_python_exception &pye)
> +    {
> +      gdb_assert (PyErr_Occurred () != nullptr);
> +      return -1;
> +    }
> +  catch (const gdb_exception &exc)
> +    {
> +      return gdbpy_handle_gdb_exception (-1, exc);
> +    }
> +
> +  return 0;
> +}
> +
> +#endif /* GDB_PYTHON_PY_SAFETY_H */
> diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
> index 6df0c62e2b3..e4f35f8cd88 100644
> --- a/gdb/python/python-internal.h
> +++ b/gdb/python/python-internal.h
> @@ -1383,5 +1383,6 @@ py_notimplemented ()
>  #undef Py_RETURN_NOTIMPLEMENTED
>  
>  #include "py-wrappers.h"
> +#include "py-safety.h"
>  
>  #endif /* GDB_PYTHON_PYTHON_INTERNAL_H */
>
> -- 
> 2.49.0
  
Tom Tromey May 21, 2026, 9:23 p.m. UTC | #3
>> +noargs_method (std::string_view name, std::string_view doc)

Andrew> Using std::string_view seems a little suspect here.

Yeah.  I think some earlier attempt at this required string_view, but
I've changed it to 'const char *' and it's fine.

>> +/* A function that wraps a "repr" or "str" method.  */
>> +template<typename C, auto M>
>> +PyObject *
>> +wrap_repr (PyObject *arg)

Andrew> Maybe something like 'wrap_tp_callback' might be better?  I just know
Andrew> every time I see 'wrap_repr' for something that isn't 'repr' I'm going
Andrew> to assume copy & paste error, and then have to go and remind myself that
Andrew> 'wrap_repr' has wider uses.  Now's a great time to find a better name
Andrew> before this starts getting used more widely..

I changed it to wrap_tp_callback.

Tom
  
Tom Tromey May 21, 2026, 10:41 p.m. UTC | #4
>> Note this patch is not 100% complete.  There should be one more
>> wrapper for case where a method takes a single argument (though we
>> probably cannot use METH_O unfortunately).  There may be some other
>> holes as well.

Andrew> Maybe reword this so it doesn't imply that THIS patch is not complete,
Andrew> but rather the implementation as a whole is not complete and will
Andrew> require future follow on patches.  What you've got is good enough to
Andrew> start using it.

I updated the text a bit.

>> +template<typename T, typename = std::is_integral<T>>

Andrew> I think the SFINAE part here is wrong, like in the previous commit.  I
Andrew> think gdb::Requires<std::is_integral<T>> might be what you mean.

Fixed.

Andrew> None of the to_python functions check their return values for error, for
Andrew> example PyUnicode_Decode can fail and return NULL, but you don't check
Andrew> for this.

Andrew> But this is OK.  to_python is only used at the point where we transition
Andrew> back from GDB's C++ code to the Python internals, so if PyUnicode_Decode
Andrew> (for example) returns NULL and sets an exception, this will be caught by
Andrew> Python.

Andrew> I have two pieces of feedback on this:

Andrew>  1. I think this should be explicitly called out in the comment above
Andrew>     the to_python functions, rather than making everyone figure out that
Andrew>     this is not a mistake.

I updated the comment that precedes the to_python functions as a whole.

>> +  /* Note that this cannot fail.  */

Andrew>     Is (IMHO) confusing.  It implies the lack of error checking here is
Andrew>     because PyBool_FromLong cannot fail, which is why, when I look at
Andrew>     later to_python functions which include calls that *can* fail, I
Andrew>     asked myself, where's the error checking.

Andrew>     I think this comment should just go.

Deleted.

>> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls

Andrew> typo: ".... then THEN calls ..."

Fixed.

>> +/* An instantiation of this function is used when calling a gdb method
>> +   from Python.  It accepts some number of arguments (normally
>> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
>> +   the underlying function F.  Any exceptions are caught and
>> +   converted, and the return value of F is converted to a Python
>> +   object as appropriate.  */

Andrew> This comment needs updating.  It also include the 'then then' typo from
Andrew> the wrapped_function comment, but also references function F, when it
Andrew> should be taking about method CLASS::METH or maybe just METH?  Anyway,
Andrew> certainly not F.

Fixed.

>> +/* A function that wraps a "repr" or "str" method.  */
>> +template<typename C, auto M>

Andrew> In wrap_setter below you are explicit about the signature of M.  I much
Andrew> prefer the explicit form, but here in wrap_repr and in wrap_getter you
Andrew> use 'auto'.  Could we switch to the explicit form in these two too?

There are two issues with changing.

One is that while a setter should probably just return void, a getter
could return anything.  And, requiring a specific return type for tp_str
or tp_repr seems a bit heavy, like maybe it would be convenient to
return std::string in some spots or gdbpy_ref<> in others.

The other issue is that 'auto' means it automatically accepts const- or
non-const-methods.  This can be handled by overloads of course.

Anyway I left this as is but we can discuss further if you want.

Tom
  
Andrew Burgess May 29, 2026, 3:58 p.m. UTC | #5
Tom Tromey <tom@tromey.com> writes:

>>> Note this patch is not 100% complete.  There should be one more
>>> wrapper for case where a method takes a single argument (though we
>>> probably cannot use METH_O unfortunately).  There may be some other
>>> holes as well.
>
> Andrew> Maybe reword this so it doesn't imply that THIS patch is not complete,
> Andrew> but rather the implementation as a whole is not complete and will
> Andrew> require future follow on patches.  What you've got is good enough to
> Andrew> start using it.
>
> I updated the text a bit.
>
>>> +template<typename T, typename = std::is_integral<T>>
>
> Andrew> I think the SFINAE part here is wrong, like in the previous commit.  I
> Andrew> think gdb::Requires<std::is_integral<T>> might be what you mean.
>
> Fixed.
>
> Andrew> None of the to_python functions check their return values for error, for
> Andrew> example PyUnicode_Decode can fail and return NULL, but you don't check
> Andrew> for this.
>
> Andrew> But this is OK.  to_python is only used at the point where we transition
> Andrew> back from GDB's C++ code to the Python internals, so if PyUnicode_Decode
> Andrew> (for example) returns NULL and sets an exception, this will be caught by
> Andrew> Python.
>
> Andrew> I have two pieces of feedback on this:
>
> Andrew>  1. I think this should be explicitly called out in the comment above
> Andrew>     the to_python functions, rather than making everyone figure out that
> Andrew>     this is not a mistake.
>
> I updated the comment that precedes the to_python functions as a whole.
>
>>> +  /* Note that this cannot fail.  */
>
> Andrew>     Is (IMHO) confusing.  It implies the lack of error checking here is
> Andrew>     because PyBool_FromLong cannot fail, which is why, when I look at
> Andrew>     later to_python functions which include calls that *can* fail, I
> Andrew>     asked myself, where's the error checking.
>
> Andrew>     I think this comment should just go.
>
> Deleted.
>
>>> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
>
> Andrew> typo: ".... then THEN calls ..."
>
> Fixed.
>
>>> +/* An instantiation of this function is used when calling a gdb method
>>> +   from Python.  It accepts some number of arguments (normally
>>> +   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
>>> +   the underlying function F.  Any exceptions are caught and
>>> +   converted, and the return value of F is converted to a Python
>>> +   object as appropriate.  */
>
> Andrew> This comment needs updating.  It also include the 'then then' typo from
> Andrew> the wrapped_function comment, but also references function F, when it
> Andrew> should be taking about method CLASS::METH or maybe just METH?  Anyway,
> Andrew> certainly not F.
>
> Fixed.
>
>>> +/* A function that wraps a "repr" or "str" method.  */
>>> +template<typename C, auto M>
>
> Andrew> In wrap_setter below you are explicit about the signature of M.  I much
> Andrew> prefer the explicit form, but here in wrap_repr and in wrap_getter you
> Andrew> use 'auto'.  Could we switch to the explicit form in these two too?
>
> There are two issues with changing.
>
> One is that while a setter should probably just return void, a getter
> could return anything.  And, requiring a specific return type for tp_str
> or tp_repr seems a bit heavy, like maybe it would be convenient to
> return std::string in some spots or gdbpy_ref<> in others.
>
> The other issue is that 'auto' means it automatically accepts const- or
> non-const-methods.  This can be handled by overloads of course.
>
> Anyway I left this as is but we can discuss further if you want.

No, that's fine, thanks for explaining the benefit here.  I'm perfectly
happy with 'auto' when it's serving a good purpose like this.

Thanks,
Andrew
  

Patch

diff --git a/gdb/python/py-safety.h b/gdb/python/py-safety.h
new file mode 100644
index 00000000000..62018dacc0f
--- /dev/null
+++ b/gdb/python/py-safety.h
@@ -0,0 +1,320 @@ 
+/* Wrappers for some Python safety.
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#ifndef GDB_PYTHON_PY_SAFETY_H
+#define GDB_PYTHON_PY_SAFETY_H
+
+#include "gdbsupport/traits.h"
+#include "py-ref.h"
+#include "py-wrappers.h"
+#include "charset.h"
+
+/* This file holds wrapper templates for the various ways that gdb
+   code might be exposed to Python.  These wrappers are part of gdb's
+   "Python safety" approach -- utilities designed to try to prevent
+   refcount problems, missing error checks, and that also remove the
+   need to wrap calls into gdb in an explicit try/catch.
+
+   See py-wrappers.h for some more discussion of this.
+
+   Implementation functions and methods -- the stuff you write to
+   expose some part of gdb to Python -- are written in a certain
+   style.
+
+   An implementation function is a module-level function.  For example
+   something like 'gdb.register_window_type' is implemented as a
+   function.  An implementation method is a method of the gdb class
+   implementing a certain Python type; for example
+   'gdb.TuiWindow.width' is implemented via a method of
+   gdbpy_tui_window.
+
+   Each of these will accept gdbpy_borrowed_ref arguments (or in some
+   more limited situations, a gdbpy_opt_borrowed_ref) and return any
+   relevant type, which will be automatically converted (see the
+   'to_python' overloads below) to the correct Python type.  Note that
+   if the 'to_python' functions aren't appropriate in your case, you
+   can always use the escape hatch of returning a gdbpy_ref<> and
+   handling type conversion manually.
+
+   There are some constexpr functions below that wrap the
+   implementation methods, and create PyMethodDef objects and the
+   like.
+
+   Implementation methods are expected to use the wrappers in
+   py-wrappers.h and not generally call into Python directly.
+
+   Implementation details of the method-wrapping safety code are put
+   into this namespace, just to emphasize that these shouldn't be used
+   elsewhere.  Skip past the namespace to find the public APIs.  */
+namespace safety_details
+{
+/* Overloads of "to_python" are used by the safety wrappers to convert
+   a function's real return value to a Python object.  A new reference
+   will always be returned.  For the time being these are kept as a
+   detail of the method-wrapping code.  However we may want to
+   consider exposing these more generally.  */
+
+static inline PyObject *
+to_python (bool value)
+{
+  /* Note that this cannot fail.  */
+  return PyBool_FromLong (value);
+}
+
+template<typename T, typename = std::is_integral<T>>
+static inline PyObject *
+to_python (T value)
+{
+  if constexpr (std::is_signed<T>::value)
+    return gdb_py_object_from_longest (value).release ();
+  else
+    return gdb_py_object_from_ulongest (value).release ();
+}
+
+static inline PyObject *
+to_python (const char *value)
+{
+  if (value == nullptr)
+    return py_none ().release ();
+  return PyUnicode_Decode (value, strlen (value), host_charset (), nullptr);
+}
+
+static inline PyObject *
+to_python (std::string &&value)
+{
+  return PyUnicode_Decode (value.c_str (), value.size (),
+			   host_charset (), nullptr);
+}
+
+static inline PyObject *
+to_python (const std::string &value)
+{
+  return PyUnicode_Decode (value.c_str (), value.size (),
+			   host_charset (), nullptr);
+}
+
+static inline PyObject *
+to_python (gdb::unique_xmalloc_ptr<char> &&value)
+{
+  if (value == nullptr)
+    return py_none ().release ();
+  return PyUnicode_Decode (value.get (), strlen (value.get ()),
+			   host_charset (), nullptr);
+}
+
+static inline PyObject *
+to_python (gdbpy_ref<> &&value)
+{
+  return value.release ();
+}
+
+/* An instantiation of this function is used when calling a gdb method
+   from Python.  It accepts some number of arguments (normally
+   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
+   the underlying function F.  Any exceptions are caught and
+   converted, and the return value of F is converted to a Python
+   object as appropriate.  */
+template<auto F, typename... Args>
+PyObject *
+wrapped_function (Args... args)
+{
+  try
+    {
+      using result_type = typename std::invoke_result_t<decltype (F), Args...>;
+
+      if constexpr (std::is_void_v<result_type>)
+	{
+	  F (args...);
+	  return py_none ().release ();
+	}
+      else
+	return to_python (F (args...));
+    }
+  catch (const gdb_python_exception &pye)
+    {
+      gdb_assert (PyErr_Occurred () != nullptr);
+      return nullptr;
+    }
+  catch (const gdb_exception &exc)
+    {
+      return gdbpy_handle_gdb_exception (nullptr, exc);
+    }
+}
+
+/* An instantiation of this function is used when calling a gdb method
+   from Python.  It accepts some number of arguments (normally
+   gdbpy_borrowed_ref or gdbpy_opt_borrowed_ref), and then then calls
+   the underlying function F.  Any exceptions are caught and
+   converted, and the return value of F is converted to a Python
+   object as appropriate.  */
+template<typename Class, typename Ret, typename... Args>
+PyObject *
+wrapped_method (Ret (Class::*meth) (Args...), Class *self, Args... args)
+{
+  try
+    {
+      if constexpr (std::is_void_v<Ret>)
+	{
+	  (self->*meth) (args...);
+	  return py_none ().release ();
+	}
+      else
+	return to_python ((self->*meth) (args...));
+    }
+  catch (const gdb_python_exception &pye)
+    {
+      gdb_assert (PyErr_Occurred () != nullptr);
+      return nullptr;
+    }
+  catch (const gdb_exception &exc)
+    {
+      return gdbpy_handle_gdb_exception (nullptr, exc);
+    }
+}
+
+template<auto F>
+PyObject *
+fn_wrapper (PyObject *self, PyObject *args, PyObject *kw)
+{
+  return wrapped_function<F> (gdbpy_borrowed_ref (args),
+			      gdbpy_opt_borrowed_ref (kw));
+}
+
+template<typename C, auto M>
+PyObject *
+varargs_wrapper (PyObject *self, PyObject *args, PyObject *kw)
+{
+  return wrapped_method (M, static_cast<C *> (self),
+			 gdbpy_borrowed_ref (args),
+			 gdbpy_opt_borrowed_ref (kw));
+}
+
+} /* namespace safety_details */
+
+/* Create a PyMethodDef for a no-argument method.  It takes the
+   underlying class C and a pointer-to-method M as template
+   parameters, and the name and documentation as arguments.  The
+   method M is wrapped to call to_python and to catch exceptions per
+   the safety protocol.  */
+template<typename C, auto M>
+constexpr PyMethodDef
+noargs_method (std::string_view name, std::string_view doc)
+{
+  using namespace safety_details;
+  return {
+    name.data (),
+    [] (PyObject *self, PyObject *args) -> PyObject *
+    {
+      return wrapped_method (M, static_cast<C *> (self));
+    },
+    METH_NOARGS,
+    doc.data (),
+  };
+}
+
+/* This is used to create the PyMethodDef for a varargs function.  It
+   takes the underlying implementation function as a template
+   argument, and also arguments for the method name and documentation
+   string.
+
+   The underlying function should accept a gdbpy_borrowed_ref argument
+   (the arguments to the Python function), and then a
+   gdbpy_opt_borrowed_ref for the keywords.  The function can return
+   any type (see the to_python overloads); and should throw an
+   exception on error.  If gdb_python_exception is thrown, the Python
+   exception must already have been set.
+
+   The gdb policy is that varargs methods must also accept keywords,
+   and this is enforced here.  */
+template<auto F>
+constexpr PyMethodDef
+varargs_function (std::string_view name, std::string_view doc)
+{
+  using namespace safety_details;
+  return {
+    name.data (),
+    (PyCFunction) fn_wrapper<F>,
+    /* gdb's rule is that varargs should also use keywords.  */
+    METH_VARARGS | METH_KEYWORDS,
+    doc.data (),
+  };
+}
+
+/* Like a varargs function, but this implements a method on some
+   Python type that gdb provides.  The implementation class and a
+   pointer-to-method must be specified.  */
+template<typename C, auto M>
+constexpr PyMethodDef
+varargs_method (std::string_view name, std::string_view doc)
+{
+  using namespace safety_details;
+  return {
+    name.data (),
+    (PyCFunction) varargs_wrapper<C, M>,
+    /* gdb's rule is that varargs should also use keywords.  */
+    METH_VARARGS | METH_KEYWORDS,
+    doc.data (),
+  };
+}
+
+/* A function that wraps a "repr" or "str" method.  */
+template<typename C, auto M>
+PyObject *
+wrap_repr (PyObject *arg)
+{
+  using namespace safety_details;
+  return wrapped_method (M, static_cast<C *> (arg));
+}
+
+/* A function that wraps a "get" method.  */
+template<typename C, auto M>
+PyObject *
+wrap_getter (PyObject *arg, void *closure)
+{
+  using namespace safety_details;
+  /* In gdb the closure argument is never used.  */
+  return wrapped_method (M, static_cast<C *> (arg));
+}
+
+/* A function that wraps a "set" method.  */
+template<typename C, void (C::*M) (gdbpy_opt_borrowed_ref)>
+int
+wrap_setter (PyObject *arg, PyObject *value, void *closure)
+{
+  using namespace safety_details;
+  try
+    {
+      C *self = static_cast<C *> (arg);
+      /* In gdb the closure argument is never used.  */
+      (self->*M) (gdbpy_opt_borrowed_ref (value));
+    }
+  catch (const gdb_python_exception &pye)
+    {
+      gdb_assert (PyErr_Occurred () != nullptr);
+      return -1;
+    }
+  catch (const gdb_exception &exc)
+    {
+      return gdbpy_handle_gdb_exception (-1, exc);
+    }
+
+  return 0;
+}
+
+#endif /* GDB_PYTHON_PY_SAFETY_H */
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 6df0c62e2b3..e4f35f8cd88 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -1383,5 +1383,6 @@  py_notimplemented ()
 #undef Py_RETURN_NOTIMPLEMENTED
 
 #include "py-wrappers.h"
+#include "py-safety.h"
 
 #endif /* GDB_PYTHON_PYTHON_INTERNAL_H */