diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d320000 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 4.3) +project(PythonCppExtension) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(Python3 "3.14" COMPONENTS Interpreter Development) + +set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdiagnostics-color=always") +set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always") + +#set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fsanitize=leak") +#set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=leak") + +set(SRCS + "src/gdb-others.cpp" + "src/py-obj-type.cpp" + "src/python-internal.cpp" + "src/my_py_module.cpp" + "src/A.cpp" + "src/B.cpp" + "src/ObjWithAlloc.cpp") +add_library(MyModule SHARED ${SRCS}) +set_target_properties( + MyModule + PROPERTIES + PREFIX "" + OUTPUT_NAME "MyModule" + LINKER_LANGUAGE C + LINK_LIBRARIES Python3::Python + INCLUDE_DIRECTORIES "${PYTHON_INCLUDE_DIR};${CMAKE_CURRENT_SOURCE_DIR}/include;${CMAKE_CURRENT_SOURCE_DIR}/src" + COMPILE_DEFINITIONS "Py_LIMITED_API=0x030e0000" + ) + +add_executable(PyDebug "pydbg.cpp") +target_link_libraries(PyDebug ${PYTHON_LIBRARIES}) +set_target_properties( + PyDebug + PROPERTIES + PREFIX "" + OUTPUT_NAME "PyDebug" + LINKER_LANGUAGE C + LINK_LIBRARIES Python3::Python + INCLUDE_DIRECTORIES "${PYTHON_INCLUDE_DIR};${CMAKE_CURRENT_SOURCE_DIR}" + ) + +add_custom_target(test-pydebug + COMMAND cmake -E env PYTHONPATH=${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/PyDebug 2>&1 + VERBATIM) +add_dependencies(test-pydebug PyDebug) + +add_custom_target(test-py + COMMAND cmake -E env PYTHONPATH=${CMAKE_BINARY_DIR} python3 ${CMAKE_CURRENT_SOURCE_DIR}/test.py + VERBATIM) +add_dependencies(test-py MyModule) diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc9fafe --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +## Build + +```bash +cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -B build-dir +``` + +## Testing + +```bash +cd build-dir +ninja && ninja test-pydebug +# OR to get the error message that is swalled by CMake +PYTHONPATH=$(pwd) ./PyDebug +``` + +### Expected output + +```text +['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', '_cycle', 'foo', 'iama', 'iamb', 'name'] +B Hello I am A Hello I am both A and B +Calling E::tp_traverse_impl +--> B_traverse +E: calling MyModule.A::tp_traverse +Calling E::tp_traverse_impl +--> A_traverse +visiting __dict__ +E: calling Py_VISIT (Py_TYPE (self)) +E: skipping Py_VISIT (Py_TYPE (self)), will call it in MyModule.A +Calling E::tp_traverse_impl +--> B_traverse +E: calling MyModule.A::tp_traverse +Calling E::tp_traverse_impl +--> A_traverse +visiting __dict__ +E: calling Py_VISIT (Py_TYPE (self)) +E: skipping Py_VISIT (Py_TYPE (self)), will call it in MyModule.A +Calling (null)::tp_clear_impl +--> B_clear +(null): calling MyModule.A::tp_clear +Unexcepted error occurred (TO INVESTIGATE) +AttributeError: __module__ +Calling (null)::tp_clear_impl +--> A_clear +clearing __dict__ +Exception ignored in tp_clear of E: +AttributeError: __module__ +Calling (null)::tp_dealloc +Unexcepted error occurred (TO INVESTIGATE) +AttributeError: __module__ +Calling (null)::tp_clear_impl +--> B_clear +(null): calling MyModule.A::tp_clear +Unexcepted error occurred (TO INVESTIGATE) +AttributeError: __module__ +Calling (null)::tp_clear_impl +--> A_clear +clearing __dict__ +Deleting type (null) +``` + +### Issues + +1. Not sure why A_clear and B_clear are called twice. Is it due to the cycle ? +2. The type name is `(null)` because `PyType_GetFullyQualifiedName()` returns `NULL` for an unknown reason. Please could you explain me why ? +3. There is an unexpected error `AttributeError: __module__`. I don't understand where it comes from. diff --git a/include/gdb-others.hpp b/include/gdb-others.hpp new file mode 100644 index 0000000..36798b3 --- /dev/null +++ b/include/gdb-others.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +extern "C" { + +#define ATTRIBUTE_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) +#define ATTRIBUTE_USED __attribute__ ((__used__)) +#define ATTRIBUTE_UNUSED __attribute__((unused)) + +#define gdb_assert(expr) \ + ((void) ((expr) ? 0 : \ + (gdb_assert_fail (#expr, __FILE__, __LINE__, __func__), 0))) + +/* This prints an "Assertion failed" message, asking the user if they + want to continue, dump core, or just exit. */ +#define gdb_assert_fail(assertion, file, line, function) \ + internal_error_loc (file, line, "%s: Assertion `%s' failed.", \ + function, assertion) + +/* The canonical form of gdb_assert (0). + MESSAGE is a string to include in the error message. */ + +#define gdb_assert_not_reached(message, ...) \ + internal_error_loc (__FILE__, __LINE__, _("%s: " message), __func__, \ + ##__VA_ARGS__) + +[[noreturn]] extern void internal_error_loc (const char *file, int line, + const char *fmt, ...) + __attribute__ ((format (printf, 3, 4)));; + +} + + +/* Wrap PyGetSetDef to allow convenient construction with string + literals. Unfortunately, PyGetSetDef's 'name' and 'doc' members + are 'char *' instead of 'const char *', meaning that in order to + list-initialize PyGetSetDef arrays with string literals (and + without the wrapping below) would require writing explicit 'char *' + casts. Instead, we extend PyGetSetDef and add constexpr + constructors that accept const 'name' and 'doc', hiding the ugly + casts here in a single place. */ + +struct gdb_PyGetSetDef : PyGetSetDef +{ + constexpr gdb_PyGetSetDef (const char *name_, getter get_, setter set_, + const char *doc_, void *closure_) + : PyGetSetDef {const_cast (name_), get_, set_, + const_cast (doc_), closure_} + {} + + /* Alternative constructor that allows omitting the closure in list + initialization. */ + constexpr gdb_PyGetSetDef (const char *name_, getter get_, setter set_, + const char *doc_) + : gdb_PyGetSetDef {name_, get_, set_, doc_, NULL} + {} + + /* Constructor for the sentinel entries. */ + constexpr gdb_PyGetSetDef (std::nullptr_t) + : gdb_PyGetSetDef {NULL, NULL, NULL, NULL, NULL} + {} +}; diff --git a/include/gdb_ref_ptr.hpp b/include/gdb_ref_ptr.hpp new file mode 100644 index 0000000..379850f --- /dev/null +++ b/include/gdb_ref_ptr.hpp @@ -0,0 +1,301 @@ +/* Reference-counted smart pointer class + + Copyright (C) 2016-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 . */ + +#ifndef GDBSUPPORT_GDB_REF_PTR_H +#define GDBSUPPORT_GDB_REF_PTR_H + +#include + +struct gdbpy_ref_policy; + +namespace gdb +{ + +/* An instance of this class either holds a reference to a + reference-counted object or is "NULL". Reference counting is + handled externally by a policy class. If the object holds a + reference, then when the object is destroyed, the reference is + decref'd. + + Normally an instance is constructed using a pointer. This sort of + initialization lets this class manage the lifetime of that + reference. + + Assignment and copy construction will make a new reference as + appropriate. Assignment from a plain pointer is disallowed to + avoid confusion about whether this acquires a new reference; + instead use the "reset" method -- which, like the pointer + constructor, transfers ownership. + + The policy class must provide two static methods: + void incref (T *); + void decref (T *); +*/ +template +class ref_ptr +{ +public: + + /* Befriend all instantiations of this template, so that the + templated copy constructors and assignment operators can access + the data. */ + template friend class ref_ptr; + + /* Create a new NULL instance. */ + ref_ptr () noexcept + : m_obj (NULL) + { + } + + /* Create a new NULL instance. Note that this is not explicit. */ + ref_ptr (const std::nullptr_t) noexcept + : m_obj (NULL) + { + } + + /* Create a new instance. OBJ is a reference, management of which + is now transferred to this class. */ + explicit ref_ptr (T *obj) noexcept + : m_obj (obj) + { + } + + /* Copy another instance. */ + template> + ref_ptr (const ref_ptr &other) + : m_obj (other.m_obj) + { + if (m_obj != NULL) + Policy::incref (m_obj); + } + + ref_ptr (const ref_ptr &other) + : m_obj (other.m_obj) + { + if (m_obj != NULL) + Policy::incref (m_obj); + } + + /* Transfer ownership from OTHER. */ + template> + ref_ptr (ref_ptr &&other) noexcept + : m_obj (other.m_obj) + { + other.m_obj = NULL; + } + + ref_ptr (ref_ptr &&other) noexcept + : m_obj (other.m_obj) + { + other.m_obj = NULL; + } + + /* Destroy this instance. */ + ~ref_ptr () + { + if (m_obj != NULL) + Policy::decref (m_obj); + } + + /* Copy another instance. */ + template> + ref_ptr &operator= (const ref_ptr &other) + { + /* Note that self-assignment is not checked here, as it isn't + possible: self-assignments will choose the non-template + function. */ + reset (other.m_obj); + if (m_obj != NULL) + Policy::incref (m_obj); + return *this; + } + + ref_ptr &operator= (const ref_ptr &other) + { + /* Do nothing on self-assignment. */ + if (this != &other) + { + reset (other.m_obj); + if (m_obj != NULL) + Policy::incref (m_obj); + } + return *this; + } + + /* Transfer ownership from OTHER. */ + template> + ref_ptr &operator= (ref_ptr &&other) + { + /* Note that self-assignment is not checked here, as it isn't + possible: self-assignments will choose the non-template + function. */ + reset (other.m_obj); + other.m_obj = NULL; + return *this; + } + + ref_ptr &operator= (ref_ptr &&other) + { + /* Do nothing on self-assignment. */ + if (this != &other) + { + reset (other.m_obj); + other.m_obj = NULL; + } + return *this; + } + + /* Change this instance's referent. OBJ is a reference, management + of which is now transferred to this class. */ + void reset (T *obj) + { + if (m_obj != NULL) + Policy::decref (m_obj); + m_obj = obj; + } + + operator bool () const noexcept + { + return m_obj != nullptr; + } + + /* Return this instance's referent without changing the state of + this class. */ + T *get () const noexcept + { + return m_obj; + } + + /* Return this instance's referent, and stop managing this + reference. The caller is now responsible for the ownership of + the reference. */ + ATTRIBUTE_UNUSED_RESULT T *release () noexcept + { + T *result = m_obj; + + m_obj = NULL; + return result; + } + + /* Let users refer to members of the underlying pointer. */ + T *operator-> () const noexcept + { + return m_obj; + } + + /* Acquire a new reference and return a ref_ptr that owns it. */ + template + static ref_ptr new_reference (TObj *obj) + { + Policy::incref (obj); + if constexpr (std::is_base_of::value + || std::is_base_of::value) + return ref_ptr (static_cast (obj)); + else + return ref_ptr (obj); + } + + template::value>> + int visit (typename P::visitproc visit, void *arg) noexcept + { + return P::visit (m_obj, visit, arg); + } + + template::value>> + void clear (P * = nullptr) noexcept + { + P::clear (&m_obj); + } + + private: + + T *m_obj; +}; + +template +inline bool operator== (const ref_ptr &lhs, + const ref_ptr &rhs) +{ + return lhs.get () == rhs.get (); +} + +template +inline bool operator== (const ref_ptr &lhs, const U *rhs) +{ + return lhs.get () == rhs; +} + +template +inline bool operator== (const ref_ptr &lhs, const std::nullptr_t) +{ + return lhs.get () == nullptr; +} + +template +inline bool operator== (const T *lhs, const ref_ptr &rhs) +{ + return lhs == rhs.get (); +} + +template +inline bool operator== (const std::nullptr_t, const ref_ptr &rhs) +{ + return nullptr == rhs.get (); +} + +template +inline bool operator!= (const ref_ptr &lhs, + const ref_ptr &rhs) +{ + return lhs.get () != rhs.get (); +} + +template +inline bool operator!= (const ref_ptr &lhs, const U *rhs) +{ + return lhs.get () != rhs; +} + +template +inline bool operator!= (const ref_ptr &lhs, const std::nullptr_t) +{ + return lhs.get () != nullptr; +} + +template +inline bool operator!= (const T *lhs, const ref_ptr &rhs) +{ + return lhs != rhs.get (); +} + +template +inline bool operator!= (const std::nullptr_t, const ref_ptr &rhs) +{ + return nullptr != rhs.get (); +} + +} + +#endif /* GDBSUPPORT_GDB_REF_PTR_H */ diff --git a/include/py-obj-dict.hpp b/include/py-obj-dict.hpp new file mode 100644 index 0000000..42154ba --- /dev/null +++ b/include/py-obj-dict.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include "py-obj-type.hpp" + +/* Helpers for Python extension objects that have a __dict__ attribute. + + Any Python C object extension (for instance, a class A inheriting PyObject) + needing __dict__ should own an attribute GDBPYOBJECT_DICT_ATTR (A). + + Access to the dict requires a custom getter defined via PyGetSetDef. + gdb_PyGetSetDef my_object_getset[] = + { + gdbpy_dict_attr_getter (A, "object"), + ... + { nullptr } + }; + + It is also important to note that __dict__ is used during the attribute + look-up. Since this dictionary is not managed by Python and is not exposed + via tp_dictoffset, custom attribute getter (tp_getattro) and setter + (tp_setattro) are required to correctly redirect attribute access to the + dictionary: + PyType_Slot object_type_slot[] = { + ..., + gdbpy_dict_attr_PyType_Slots (A), + ... + }; + + Finally, heap-allocated types owning a __dict__ should visit and clear their + __dict__. + static int + A_tp_traverse (PyObject *self, visitproc visit, void *arg) + { + auto *obj = (A *) self; + gdbpy_dict_attr_Py_VISIT (obj); + ... + return 0; + } + + static void + A_tp_clear (PyObject *self) + { + auto *obj = (A *) self; + gdbpy_dict_attr_Py_CLEAR (obj); + ... + } + */ +template +struct gdbpy_dict_helpers +{ +private: + + /* Compute the address of the __dict__ attribute for the given PyObject. */ + static PyObject **compute_addr (PyObject *self) + { + auto *dict_owner = reinterpret_cast (self); + return &dict_owner->dict; + } + +public: + + static PyObject * + allocate (PyObject *self) + { + PyObject **py_dict_ptr = gdbpy_dict_helpers::compute_addr (self); + PyObject *py_dict = *py_dict_ptr; + gdb_assert (py_dict == nullptr); + *py_dict_ptr = PyDict_New (); + py_dict = *py_dict_ptr; + return py_dict; + } + + + /* Generic implementation of the getter for the __dict__ attribute for objects + having a dictionary. The CLOSURE argument is unused. */ + + static PyObject * + getter (PyObject *self, void *closure ATTRIBUTE_UNUSED) + { + PyObject **py_dict_ptr = gdbpy_dict_helpers::compute_addr (self); + PyObject *py_dict = *py_dict_ptr; + if (py_dict == nullptr) + { + PyErr_SetString (PyExc_AttributeError, + "This object has no __dict__"); + return nullptr; + } + //if (py_dict == nullptr) + // py_dict = gdbpy_dict_helpers::allocate (self, py_dict_ptr); + return Py_NewRef (py_dict); + } + + /* Generic attribute getter function similar to PyObject_GenericGetAttr () but + that should be used when the object has a dictionary __dict__. */ + static PyObject * + getattro (PyObject *self, PyObject *attr) + { + PyObject *value = PyObject_GenericGetAttr (self, attr); + if (value != nullptr) + return value; + + if (! PyErr_ExceptionMatches (PyExc_AttributeError)) + return nullptr; + + gdbpy_ref<> dict (gdbpy_dict_helpers::getter (self, nullptr)); + if (dict == nullptr) + return nullptr; + + /* Clear previous AttributeError set by PyObject_GenericGetAttr when it + did not find the attribute, and try to get the attribute from __dict__. */ + PyErr_Clear(); + + value = PyDict_GetItemWithError (dict.get (), attr); + if (value != nullptr) + return Py_NewRef (value); + + /* If PyDict_GetItemWithError() returns NULL because an error occurred, it + sets an exception. Propagate it by returning NULL. */ + if (PyErr_Occurred () != nullptr) + return nullptr; + + /* If the key is not found, PyDict_GetItemWithError() returns NULL without + setting an exception. Failing to set one here would later result in: + : error return without exception set + Therefore, we must explicitly raise an AttributeError in this case. */ + PyErr_Format (PyExc_AttributeError, + "'%s' object has no attribute '%s'", + gdbpy_py_obj_tp_name (self), + PyUnicode_AsUTF8AndSize (attr, nullptr)); + return nullptr; + } + + /* Generic attribute setter function similar to PyObject_GenericSetAttr () but + that should be used when the object has a dictionary __dict__. */ + static int + setattro (PyObject *self, PyObject *attr, PyObject *value) + { + if (PyObject_GenericSetAttr (self, attr, value) == 0) + return 0; + + if (! PyErr_ExceptionMatches (PyExc_AttributeError)) + return -1; + + gdbpy_ref<> dict (gdbpy_dict_helpers::getter (self, nullptr)); + if (dict == nullptr) + return -1; + + /* Clear previous AttributeError set by PyObject_GenericGetAttr() when it + did not find the attribute, and try to set the attribute into __dict__. */ + PyErr_Clear(); + + /* Set the new value. + Note: the old value is managed by PyDict_SetItem(), so no need to get + a borrowed reference on it and decrement its reference counter before + setting a new value. */ + return PyDict_SetItem (dict.get (), attr, value); + } +}; + +#define GDBPYOBJECT_DICT_ATTR(O) \ + PyObject *dict; \ + using dict_attr = gdbpy_dict_helpers + +#define gdbpy_dict_attr_getter(O, object_name) \ + { \ + "__dict__", /* name */ \ + (getter) O::dict_attr::getter, \ + (setter) nullptr, \ + "The __dict__ for this " object_name ".", /* doc */ \ + nullptr, /* closure */ \ + } + +#define gdbpy_dict_attr_PyType_Slots(O) \ + {Py_tp_getattro, reinterpret_cast(O::dict_attr::getattro)}, \ + {Py_tp_setattro, reinterpret_cast(O::dict_attr::setattro)} + +#define gdbpy_dict_attr_Py_ALLOC(T, self)\ + gdbpy_dict_helpers::allocate (self) + +#define gdbpy_dict_attr_Py_VISIT(obj) \ + printf ("visiting __dict__\n"); \ + Py_VISIT (obj->dict) + +#define gdbpy_dict_attr_Py_CLEAR(obj) \ + printf ("clearing __dict__\n"); \ + Py_CLEAR (obj->dict) + +#define gdbpy_dict_attr_Py_ALLOC(T, self) \ + gdbpy_dict_helpers::allocate (self) diff --git a/include/py-obj-type.hpp b/include/py-obj-type.hpp new file mode 100644 index 0000000..0762519 --- /dev/null +++ b/include/py-obj-type.hpp @@ -0,0 +1,424 @@ +/* Helpers related to Python object type + + 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 . */ + +#ifndef GDB_PYTHON_PY_OBJ_TYPE_H +#define GDB_PYTHON_PY_OBJ_TYPE_H + +#include "py-ref.hpp" +#include +#include + +/* Return the type's fully qualified name from a PyTypeObject. */ +extern std::string gdb_py_tp_name (PyTypeObject *py_type) noexcept; + +/* Return the type's fully qualified name from a PyObject. */ +extern std::string gdbpy_py_obj_tp_name (PyObject *self) noexcept; + +using gdbpy_bref_pytype = gdbpy_borrowed_ref; + +namespace gdbpy { + template + struct is_pyobject_wrapper: std::false_type {}; + + template + struct is_pyobject_wrapper>: std::true_type {}; + + template + struct is_pyobject_wrapper>: std::true_type {}; + + template + constexpr bool is_pyobject_wrapper_v = is_pyobject_wrapper::value; +} // namespace gdbpy + +template +struct gdbpy_object_type +{ +private: + PyTypeObject *pytype_ () const noexcept + { + return ((TypeWrapper *) this)->pytype (); + } + +public: + + operator PyTypeObject *() const noexcept + { + return this->pytype_ (); + } + + std::string tp_name () const noexcept + { + return gdb_py_tp_name (this->pytype_ ()); + } + + bool operator== (const TypeWrapper &other) const noexcept + { + return this->pytype_ () == other.pytype_ (); + } + + bool same_as (PyTypeObject *objtype) const noexcept + { + return objtype == this->pytype_ (); + } + + bool same_as (PyObject *obj) const noexcept + { + /* Note: if the type objects are the same, their address should be + the same because GDB manages all those types as singletons. */ + return (obj != nullptr + && ((PyType_Check (obj) != 0 + && this->same_as (reinterpret_cast (obj))) + || this->same_as (Py_TYPE (obj)))) + || this->pytype_ () == nullptr; + } + + bool same_as (std::nullptr_t) const noexcept + { + return this->pytype_ () == nullptr; + } + + bool same_as (const gdbpy_borrowed_ref &wrapper) const noexcept + { + return this->pytype_ () == wrapper.get (); + } + + template + bool same_as (const gdbpy_ref &wrapper) const noexcept + { + return this->same_as (static_cast (wrapper.get ())); + } + + void *slot (int slot_id) const noexcept + { + return PyType_GetSlot (this->pytype_ (), slot_id); + } + + gdbpy_borrowed_ref base_type () const noexcept + { + return gdbpy_borrowed_ref + ((PyTypeObject *) this->slot (Py_tp_base)); + } + + bool heap_type () const noexcept + { + return PyType_GetFlags (this->pytype_ ()) | Py_TPFLAGS_HEAPTYPE; + } +}; + +struct gdbpy_object_type_ref: + public gdbpy_object_type , + public gdbpy_ref <> +{ + gdbpy_object_type_ref () noexcept = default; + gdbpy_object_type_ref (gdbpy_object_type_ref &&other) noexcept = default; + gdbpy_object_type_ref& operator= (gdbpy_object_type_ref &&other) noexcept = default; + + gdbpy_object_type_ref (std::nullptr_t) noexcept + : gdbpy_ref<> (nullptr) + { + } + + gdbpy_object_type_ref (gdbpy_ref <> &&other) noexcept + : gdbpy_ref<> (std::move(other)) + { + gdb_assert (PyType_Check (this->get ()) != 0); + } + + PyTypeObject *pytype () const noexcept + { + return reinterpret_cast (this->get ()); + } +}; + +struct gdbpy_object_type_bref: + public gdbpy_object_type , + public gdbpy_borrowed_ref <> +{ + gdbpy_object_type_bref () noexcept = default; + gdbpy_object_type_bref (const gdbpy_object_type_bref &other) noexcept = default; + gdbpy_object_type_bref& operator= (const gdbpy_object_type_bref &other) noexcept = default; + + gdbpy_object_type_bref (const gdbpy_object_type_ref& other) noexcept + : gdbpy_borrowed_ref<> (other.get ()) + { + } + + gdbpy_object_type_bref (std::nullptr_t) noexcept + : gdbpy_borrowed_ref<> (nullptr) + { + } + + PyTypeObject *pytype () const noexcept + { + if (this->get () == nullptr) + return nullptr; + return reinterpret_cast (this->get ()); + } +}; + +struct gdbpy_py_obj_type_bref: + public gdbpy_object_type , + public gdbpy_borrowed_ref +{ + gdbpy_py_obj_type_bref () noexcept = default; + gdbpy_py_obj_type_bref (const gdbpy_py_obj_type_bref &other) noexcept = default; + gdbpy_py_obj_type_bref& operator= (gdbpy_py_obj_type_bref &&other) noexcept = default; + + gdbpy_py_obj_type_bref (const gdbpy_borrowed_ref &pytype) noexcept + : gdbpy_borrowed_ref (pytype) + { + } + + gdbpy_py_obj_type_bref (PyTypeObject *pytype) noexcept + : gdbpy_borrowed_ref (pytype) + { + } + + PyTypeObject *pytype () const noexcept + { + return this->get (); + } +}; + +/* Wrapper around PyObject_TypeCheck(). + Note: this wrapper will be decommissioned once all the Python extension + types will have been migrated to the Python limited API. */ +inline bool +gdbpy_type_check (PyObject *obj, const PyTypeObject &obj_type) +{ + return PyObject_TypeCheck (obj, const_cast (&obj_type)); +} + +/* Wrapper around PyObject_TypeCheck() to compare against any wrapper + convertible to 'PyTypeObject *'. */ +template > +inline bool +gdbpy_type_check (PyObject *obj, const OType &obj_type) +{ + return PyObject_TypeCheck (obj, obj_type.pytype ()); +} + +/* Wrapper around PyObject_TypeCheck() similar to the previous one, but + accepting 'gdbpy_ref' and 'gdbpy_borrowed_ref' instead of raw + 'PyObject *'. */ +template , + typename = std::is_convertible> +inline bool +gdbpy_type_check (const Wrapper &obj, const OType &obj_type) +{ + return PyObject_TypeCheck (obj.get (), obj_type.pytype ()); +} + +struct gdbpy_heap_type +{ + using tp_traverse_t = int (*) (PyObject *self, visitproc visit, void *arg); + using tp_clear_t = void (*) (PyObject *self); + + static constexpr unsigned long + tp_flags_default () + { return Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC; } + + static int + tp_traverse (PyObject *self, visitproc visit, void *arg, + const gdbpy_object_type_bref &base_type, + tp_traverse_t tp_traverse_impl) + { + int res = 0; + + /* Traverse the object's attributes. */ + if (tp_traverse_impl != nullptr) + { + if ((res = tp_traverse_impl (self, visit, arg)) != 0) + return res; + } + + /* Don't forget to call tp_traverse of the base class. */ + if (base_type != nullptr) + { + auto base_tp_traverse + = (gdbpy_heap_type::tp_traverse_t) base_type.slot (Py_tp_traverse); + if ((res = base_tp_traverse (self, visit, arg)) != 0) + return res; + } + + /* From Python 3.9, tp_traverse must visit the type for heap-allocated + types. Older versions must not visit it. + Note that, in case of inheritance, the type should be visited only + once. */ +#if PY_VERSION_HEX >= 0x03090000 + if (base_type == nullptr) + { + printf ("calling Py_VISIT (Py_TYPE (self))\n"); + Py_VISIT (Py_TYPE (self)); + } +#endif + + return res; + } + + static void + tp_clear (PyObject *self, + const gdbpy_object_type_bref &base_type, + tp_clear_t tp_clear_impl) + { + /* Clear any fields specific to the type. */ + if (tp_clear_impl != nullptr) + tp_clear_impl (self); + + /* Don't forget to call tp_clear of the base class. */ + if (base_type != nullptr) + { + auto base_tp_clear + = (gdbpy_heap_type::tp_clear_t) base_type.slot (Py_tp_clear); + base_tp_clear (self); + } + } + + static void + tp_dealloc (PyObject *self) + { + auto *tp = Py_TYPE (self); + + /* Call PyObject_GC_UnTrack() before any fields are invalidated. */ + PyObject_GC_UnTrack (self); + + /* Clear the object's attributes. */ + auto self_tp_clear = (tp_clear_t) PyType_GetSlot (tp, Py_tp_clear); + if (self_tp_clear) + self_tp_clear (self); + + /* Delete the object. */ + PyObject_GC_Del (self); + + /* Decrement the reference count of the type. */ + printf ("decrement ref count on Py_TYPE (self)\n"); + Py_DECREF (tp); + } + + /* A convenient wrapper enforcing the call to PyObject_GC_Track() on a newly + allocated Python object before it goes out of scope. If a developer + forgets to call track(), the destructor raises a runtime assertion in the + destructor. */ + template + struct auto_gc_track + : private gdbpy_ref + { + auto_gc_track (T *obj) + : gdbpy_ref (obj) + {} + + auto_gc_track (auto_gc_track &&other) + : gdbpy_ref (std::move (other)) + {} + + auto_gc_track &operator= (auto_gc_track &&other) + { + if (this != other) + gdbpy_ref::operator= (std::move (other)); + return *this; + } + + gdbpy_borrowed_ref borrowed_ref () const noexcept + { + return gdbpy_ref::get (); + } + + gdbpy_ref strong_ref () const noexcept + { + return gdbpy_ref(*this); + } + + /* Start tracking of the newly allocated object with the GC. + Return gdbpy_ref as the finalized object. */ + gdbpy_ref track () + { + PyObject_GC_Track (this->get ()); + return std::move (*this); + } + + /* Bail out, i.e. free the newly allocated object and return NULL. */ + std::nullptr_t bailout () + { + auto obj = this->track (); + return nullptr; + } + + inline explicit operator bool () const noexcept + { + return gdbpy_ref::get () != nullptr; + } + + inline bool operator== (const std::nullptr_t) const noexcept + { + return gdbpy_ref::get () == nullptr; + } + + T *operator-> () const noexcept + { + return gdbpy_ref::get (); + } + + ~auto_gc_track () + { + /* The object should have been tracked by the GC, and released before + this destructor is called. */ + gdb_assert (gdbpy_ref::get () == nullptr); + } + }; + + /* Convenient wrapper to PyObject_GC_New() returning an auto_gc_track + object, used to enforce the call to PyObject_GC_Track() on the newly + allocated Python object before auto_gc_track goes out of scope. */ + template + static auto_gc_track + make_pyobj (const gdbpy_object_type_bref &obj_type) + { + auto_gc_track obj + (static_cast (PyObject_GC_New (T, obj_type.pytype ()))); + return obj; + } +}; + +/* Helper macro to initialize mandatory slot IDs (i.e. Py_tp_traverse, + Py_tp_clear, and Py_tp_dealloc) for garbage-collected types, using + default methods. + Note: those defaults are ideal for classes containing no strong + references of Python objects in their attributes. */ + +/* Helper macro to initialize mandatory slot IDs (i.e. Py_tp_traverse, + Py_tp_clear, and Py_tp_dealloc) for garbage-collected types, providing + custom methods for tp_traverse and tp_clear. */ +#define gdbpy_heap_type_methods(fn_traverse, fn_clear) \ + {Py_tp_traverse, (void *) fn_traverse}, \ + {Py_tp_clear, (void *) fn_clear}, \ + {Py_tp_dealloc, (void *) gdbpy_heap_type::tp_dealloc} + +/* Helper macro declaring 'varname', creating a new heap-allocated object of + static type OBJTYPE, initializing it following specs declared in py_objtype + (the dynamically-allocated type corresponding to OBJTYPE), and validating + its creation or bailing out. */ +#define gdbpy_heap_type_allocate_or_bailout(varname, OBJTYPE, py_objtype)\ + auto varname = gdbpy_heap_type::make_pyobj (py_objtype); \ + if (varname == nullptr) \ + return nullptr + +#endif /* GDB_PYTHON_PY_OBJ_TYPE_H */ diff --git a/include/py-ref.hpp b/include/py-ref.hpp new file mode 100644 index 0000000..c8ea3a0 --- /dev/null +++ b/include/py-ref.hpp @@ -0,0 +1,204 @@ +/* Python reference-holding class + + Copyright (C) 2016-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 . */ + +#ifndef GDB_PYTHON_PY_REF_H +#define GDB_PYTHON_PY_REF_H + +#include "gdb_ref_ptr.hpp" +#include +#include "python-traits.hpp" + +/* A policy class for gdb::ref_ptr for Python reference counting. */ +struct gdbpy_ref_policy +{ + static void incref (PyObject *ptr) + { + Py_INCREF (ptr); + } + + static void decref (PyObject *ptr) + { + Py_DECREF (ptr); + } + + using visitproc = int (*)(PyObject *object, void *arg); + + static int visit (PyObject *ptr, visitproc visit, void *arg) noexcept + { + Py_VISIT (ptr); + return 0; + } + + template + static void clear (T **ptr) noexcept + { + Py_CLEAR (*ptr); + } +}; + +/* Helper macro to allocate and build a new gdbpy_ref from a pointer + while incrementing the ref counter. */ +#define gdbpy_ref_NEWREF(T, ptr_obj) \ + new gdbpy_ref (gdbpy_ref::new_reference (ptr_obj)) + +/* Cousin macro of Py_VISIT specialized for gdbpy_ref. + Note: this macro has to be used inside the custom tp_traverse function for + type T so that its parameters visit and arg are known. */ +#define gdbpy_ref_VISIT(ptr_gdbpy_ref) \ + do \ + { \ + if (ptr_gdbpy_ref) \ + { \ + int ret \ + = (ptr_gdbpy_ref)->visit (visit, arg); \ + if (ret) \ + return ret; \ + } \ + } \ + while (0) + +/* Cousin macro of Py_CLEAR specialized for gdbpy_ref. */ +#define gdbpy_ref_CLEAR(obj) \ + do \ + { \ + if (obj) \ + { \ + (obj)->clear (); \ + delete (obj); \ + (obj) = nullptr; \ + } \ + } \ + while (0) + +/* A gdb::ref_ptr that has been specialized for Python objects or + their "subclasses". */ +template using gdbpy_ref + = gdb::ref_ptr; + +/* A class representing a borrowed reference. + + This is a simple wrapper for a PyObject*. Aside from documenting + what the code does, the main advantage of using this is that + conversion to a gdbpy_ref<> is guaranteed to make a new + reference. */ +template +class gdbpy_borrowed_ref +{ +public: + + gdbpy_borrowed_ref () noexcept + : m_obj (nullptr) + { + } + + gdbpy_borrowed_ref (const std::nullptr_t) noexcept + : m_obj (nullptr) + { + } + + template > + gdbpy_borrowed_ref (U *obj) noexcept + : m_obj (obj) + { + } + + template > + gdbpy_borrowed_ref (const gdbpy_ref &ref) noexcept + : m_obj (ref.get ()) + { + } + + gdbpy_borrowed_ref (const gdbpy_borrowed_ref &other) noexcept + : m_obj (other.m_obj) + { + } + + gdbpy_borrowed_ref &operator= (const gdbpy_borrowed_ref &other) + { + m_obj = other.m_obj; + return *this; + } + + operator bool () const noexcept + { + return m_obj != nullptr; + } + + operator T * () const noexcept + { + return m_obj; + } + + /* When converting a borrowed reference to a gdbpy_ref<>, a new + reference is acquired. */ + template > + operator gdbpy_ref () + { + gdb_assert (m_obj != nullptr); + return gdbpy_ref::new_reference (m_obj); + } + + gdbpy_ref strong_ref () const noexcept + { + return static_cast> (*this); + } + + /* Let users refer to members of the underlying pointer. */ + T *operator-> () const noexcept + { + return m_obj; + } + + T *get () const noexcept + { + return m_obj; + } + +private: + T *m_obj; +}; + +template +inline bool operator== (const gdbpy_borrowed_ref &lhs, const std::nullptr_t) +{ + return lhs.get () == nullptr; +} + +template +inline bool operator!= (const gdbpy_borrowed_ref &lhs, const std::nullptr_t) +{ + return lhs.get () != nullptr; +} + +template +inline bool operator== (const std::nullptr_t, const gdbpy_borrowed_ref &rhs) +{ + return nullptr == rhs.get (); +} + +template +inline bool operator!= (const std::nullptr_t, const gdbpy_borrowed_ref &rhs) +{ + return nullptr != rhs.get (); +} + +#endif /* GDB_PYTHON_PY_REF_H */ diff --git a/include/python-internal.hpp b/include/python-internal.hpp new file mode 100644 index 0000000..151ad47 --- /dev/null +++ b/include/python-internal.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include "gdb-others.hpp" +#include "py-ref.hpp" +#include "py-obj-dict.hpp" +#include "py-obj-type.hpp" + +extern gdbpy_object_type_ref +gdbpy_type_initialize (gdbpy_borrowed_ref<> &mod, + PyType_Spec *spec, + PyObject *bases = nullptr, + bool disallow_instantiation = false); diff --git a/include/python-traits.hpp b/include/python-traits.hpp new file mode 100644 index 0000000..d1d3053 --- /dev/null +++ b/include/python-traits.hpp @@ -0,0 +1,93 @@ +/* Type traits relating to GDB's Python integration. + + Copyright (C) 2008-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 . */ + +#ifndef GDB_PYTHON_PYTHON_TRAITS_H +#define GDB_PYTHON_PYTHON_TRAITS_H + +namespace gdb +{ +/* All of our custom Python types are created as structs, like this: + + struct some_new_type : public PyObject + { + ... various fields ... + }; + + Then instances of this struct are created by calling PyObject_New, + either directly within GDB's C++ code, or within Python when a user's + Python script creates an instance of that class. + + The problem is that Python is written in C, and PyObject_New doesn't + call any constructors for `some_new_type`, nor for any of the fields + within `some_new_type`. + + If `some_new_type` is Plain Old Data (POD), then this is fine. Or, to + be more C++ specific, if `some_new_type` is trivially default + constructable, then we're fine. + + But if a field within `some_new_type` has a non-trivial constructor, + then we're in trouble as that constructor will never be run. + + An example of a problematic field type is frame_info_ptr. The + constructor for this type registers the new object with a central + management object, recording the `this` pointer, using this type within + `some_new_type` will not work as expected; frame invalidation will not + show up within the frame_info_ptr as you might expect. + + And so, this type trait exists. Whenever a struct is created to define + a new Python type we should add a line like: + + static_assert (gdb::is_python_allocatable_v); + + This will fail if any field of `some_new_type` is unsuitable for this + use. + + We don't actually check is_trivially_default_constructible here. Some + types, e.g. ui_file_style::color, have non-trivial (or no default) + constructors, but are still safe to use within `some_new_type` because + their constructors just initialise data fields; there's nothing + "special" that the constructor does that cannot be achieved by + assigning the fields after creation with PyObject_New. + + What actually matters is that the type is trivially destructible + (Python won't call C++ destructors, so destructors with side effects, + like deregistering from a list, would be skipped) and trivially + copyable (Python may copy objects with memcpy). Types like + frame_info_ptr, whose constructors and destructors have side effects + such as registering with a central management object, will be caught + because they are neither trivially destructible nor trivially copyable. + Types like ui_file_style are trivially destructible and copyable, so + pass this trait. */ + +template +struct is_python_allocatable +{ + static constexpr bool value = + std::is_trivially_destructible_v + && std::is_trivially_copyable_v; +}; + +/* Helper for the above trait to make it more usable. */ + +template +inline constexpr bool is_python_allocatable_v + = is_python_allocatable::value; +} + +#endif /* GDB_PYTHON_PYTHON_TRAITS_H */ diff --git a/pydbg.cpp b/pydbg.cpp new file mode 100644 index 0000000..bcfae2e --- /dev/null +++ b/pydbg.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include + +int main(int argc, char *argv[], char *envp[]) +{ + Py_Initialize(); + + PyObject *pmodule = PyImport_ImportModule("MyModule"); + if (!pmodule) { + PyErr_Print(); + std::cerr << "Failed to import module MyModule" << std::endl; + return -1; + } + + PyObject *classA = PyObject_GetAttrString(pmodule, "A"); + if (!classA) { + std::cerr << "Unable to get type A from MyModule" << std::endl; + return -1; + } + + PyObject *classB = PyObject_GetAttrString(pmodule, "B"); + if (!classB) { + std::cerr << "Unable to get type B from MyModule" << std::endl; + return -1; + } + + PyObject *myAInstance = PyObject_CallObject(classA, NULL); + if (!myAInstance) { + std::cerr << "Instantioation of MyClass failed" << std::endl; + return -1; + } + Py_DECREF(myAInstance); + + PyObject *pGlobal = PyDict_New(); + PyObject *pValue = PyRun_String( + "import MyModule as MM\n" + "b = MM.B()\n" + "b.foo = 1234\n" + "print(b.name(), b.iamb())\n" + "\n" + "class C(MM.B):\n" + " pass\n" + "c = C()\n" + "c.foo = 1234\n" + "print(dir(c))\n" + "print(c.name(), c.iamb())\n" + "\n" + "class D(MM.A):\n" + " def __init__(self):\n" + " super().__init__()\n" + " self._cycle = self\n" + "\n" + "d = D()\n" + "d.foo = 1234\n" + "print(dir(d))\n" + "print(d.name(), d.iama())\n" + "\n" + "class E(MM.B):\n" + " def __init__(self):\n" + " super().__init__()\n" + " self._cycle = self\n" + "\n" + "e = E()\n" + "e.foo = 1234\n" + "print(dir(e))\n" + "print(e.name(), e.iama(), e.iamb())\n" + "\n" + "f = MM.ObjWithAlloc()\n" + "print('{} {}'.format(f.name(), f.whoami()))\n" + "print(f.speak())\n" + , Py_file_input, pGlobal, pGlobal); + Py_XDECREF(pValue); + Py_DECREF(pGlobal); + + Py_Finalize(); + + return 0; +} diff --git a/src/A.cpp b/src/A.cpp new file mode 100644 index 0000000..c2738a5 --- /dev/null +++ b/src/A.cpp @@ -0,0 +1,113 @@ +#include "my_py_module.hpp" + +namespace { + extern PyType_Spec A_object_type_spec; +} /* namespace anonymous */ + +namespace { + +static int +A_init (PyObject *self, PyObject *args, PyObject *kwds) +{ + auto* obj = (A *) self; + gdbpy_dict_attr_Py_ALLOC (A, self); + obj->m_a = PyUnicode_FromString ("A"); + return 0; +} + +/* The tp_traverse callback for the A type. */ +static int +A_traverse_impl (PyObject *self, visitproc visit, void *arg) +{ + auto *obj = (A *) self; + printf ("--> A_traverse_impl\n"); + gdbpy_dict_attr_Py_VISIT (obj); + Py_VISIT (obj->m_a); + return 0; +} + +static int +A_traverse (PyObject *self, visitproc visit, void *arg) +{ + return gdbpy_heap_type::tp_traverse (self, visit, arg, + nullptr, A_traverse_impl); +} + +/* The tp_clear callback for the A type. */ + +static void +A_clear_impl (PyObject *self) +{ + auto *obj = (A *) self; + printf ("--> A_clear_impl\n"); + gdbpy_dict_attr_Py_CLEAR (obj); + Py_CLEAR (obj->m_a); +} + +static void +A_clear (PyObject *self) +{ + return gdbpy_heap_type::tp_clear (self, nullptr, A_clear_impl); +} + +PyObject * +A_name (PyObject *self, PyObject *args) +{ + auto *obj = (A *) self; + Py_INCREF (obj->m_a); + return obj->m_a; +} + +PyObject * +A_iama (PyObject *self, PyObject *args) +{ + auto *obj = (A *) self; + return PyUnicode_FromFormat ("Hello I am %U", obj->m_a); +} + +gdb_PyGetSetDef A_object_getset[] = +{ + gdbpy_dict_attr_getter (A, "A"), + { nullptr } /* Sentinel */ +}; + +PyMethodDef A_object_methods [] = { + { "name", A_name, METH_NOARGS, + "name () -> String.\n\ +Return the name of the class as a string value." }, + { "iama", A_iama, METH_NOARGS, + "iama () -> String.\n\ +Return the object's greeting as a string value." }, + {nullptr} /* Sentinel */ +}; + +PyType_Slot A_object_type_slot[] = { + {Py_tp_doc, const_cast("A object")}, + gdbpy_heap_type_methods (A_traverse, A_clear), + gdbpy_dict_attr_PyType_Slots (A), + {Py_tp_init, (void*) A_init}, + {Py_tp_methods, A_object_methods}, + {Py_tp_getset, A_object_getset}, + {0, nullptr}, /* Sentinel */ +}; + +PyType_Spec A_object_type_spec = { + "MyModule.A", /* tp_name */ + sizeof (A), /* tp_basicsize */ + 0, /* tp_itemsize */ + gdbpy_heap_type::tp_flags_default () + | Py_TPFLAGS_BASETYPE, /* tp_flags */ + A_object_type_slot, +}; + +} // namespace anonymous + +gdbpy_object_type_ref +A::record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class ATTRIBUTE_UNUSED) +{ + gdbpy_object_type_ref obj_type + = gdbpy_type_initialize (module, + &A_object_type_spec); + return obj_type; +} diff --git a/src/B.cpp b/src/B.cpp new file mode 100644 index 0000000..4dfd3a9 --- /dev/null +++ b/src/B.cpp @@ -0,0 +1,116 @@ +#include "my_py_module.hpp" + +namespace { + extern PyType_Spec B_object_type_spec; + gdbpy_object_type_bref B_base_type = nullptr; +} /* namespace anonymous */ + +namespace { + +static int +B_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + auto* obj = (B *) self; + + /* super().__init__(*args, **kwargs) */ + initproc base_func_init = (initproc) B_base_type.slot (Py_tp_init); + gdb_assert (base_func_init != nullptr); + if (base_func_init (self, args, kwds) != 0) + return -1; + + obj->m_b = PyUnicode_FromString ("B"); + return 0; +} + +/* The tp_traverse callback for the A type. */ +static int +B_traverse_impl (PyObject *self, visitproc visit, void *arg) +{ + auto *obj = (B *) self; + printf ("--> B_traverse_impl\n"); + Py_VISIT (obj->m_b); + return 0; +} + +static int +B_traverse(PyObject *self, visitproc visit, void *arg) +{ + return gdbpy_heap_type::tp_traverse(self, visit, arg, + B_base_type, B_traverse_impl); +} + +/* The tp_clear callback for the A type. */ + +static void +B_clear_impl (PyObject *self) +{ + auto *obj = (B *) self; + printf ("--> B_clear_impl\n"); + Py_CLEAR (obj->m_b); +} + +static void +B_clear (PyObject *self) +{ + gdbpy_heap_type::tp_clear(self, B_base_type, B_clear_impl); +} + +PyObject * +B_name (PyObject *self, PyObject *args) +{ + auto *obj = (B *) self; + gdb_assert (obj->m_b != nullptr); + Py_INCREF (obj->m_b); + return obj->m_b; +} + +PyObject * +B_iamb (PyObject *self, PyObject *args) +{ + auto *obj = (B *) self; + gdb_assert (obj->m_a != nullptr); + gdb_assert (obj->m_b != nullptr); + return PyUnicode_FromFormat ("Hello I am both %U and %U", obj->m_a, obj->m_b); +} + +PyMethodDef B_object_methods [] = { + { "name", B_name, METH_NOARGS, + "name () -> String.\n\ +Return the name of the class as a string value." }, + { "iamb", B_iamb, METH_NOARGS, + "iamb () -> String.\n\ +Return the object's greeting as a string value." }, + {nullptr} /* Sentinel */ +}; + +PyType_Slot B_object_type_slot[] = { + {Py_tp_doc, const_cast("B object")}, + gdbpy_heap_type_methods (B_traverse, B_clear), + {Py_tp_init, (void*) B_init}, + {Py_tp_methods, B_object_methods}, + {0, nullptr}, /* Sentinel */ +}; + +PyType_Spec B_object_type_spec = { + "MyModule.B", /* tp_name */ + sizeof (B), /* tp_basicsize */ + 0, /* tp_itemsize */ + gdbpy_heap_type::tp_flags_default () + | Py_TPFLAGS_BASETYPE, /* tp_flags */ + B_object_type_slot, +}; + +} // namespace anonymous + +gdbpy_object_type_ref +B::record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref A_base_class) +{ + B_base_type = A_base_class; + + gdbpy_object_type_ref obj_type + = gdbpy_type_initialize (module, + &B_object_type_spec, + A_base_class.get ()); + return obj_type; +} diff --git a/src/ObjWithAlloc.cpp b/src/ObjWithAlloc.cpp new file mode 100644 index 0000000..ed0b377 --- /dev/null +++ b/src/ObjWithAlloc.cpp @@ -0,0 +1,140 @@ +#include "my_py_module.hpp" + +namespace { + extern PyType_Spec ObjWithAlloc_object_type_spec; + gdbpy_object_type_bref ObjWithAlloc_object_type; +} /* namespace anonymous */ + +namespace { + +static PyObject * +ObjWithAlloc_alloc () +{ + gdbpy_heap_type_allocate_or_bailout + (my_new_obj, ObjWithAlloc, ObjWithAlloc_object_type); + + my_new_obj->msg = new std::string ( + "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, " + "consectetur, adipisci velit..."); + + my_new_obj->m_name = PyUnicode_FromString ("ObjWithAlloc"); + + my_new_obj->self_cycle = my_new_obj.strong_ref ().release (); + + /* This is only an example to illustrate the usage of bailout(). */ + if (*my_new_obj->msg == "hello world !") + { + delete my_new_obj->msg; + Py_XDECREF (my_new_obj->m_name); + Py_XDECREF (my_new_obj->self_cycle); + return my_new_obj.bailout (); + } + return my_new_obj.track ().release (); +} + +/* The tp_traverse callback for the A type. */ +static int +ObjWithAlloc_traverse_impl (PyObject *self, visitproc visit, void *arg) +{ + auto *obj = (ObjWithAlloc *) self; + printf ("--> ObjWithAlloc_traverse_impl\n"); + Py_VISIT (obj->m_name); + Py_VISIT (obj->self_cycle); + return 0; +} + +static int +ObjWithAlloc_traverse (PyObject *self, visitproc visit, void *arg) +{ + return gdbpy_heap_type::tp_traverse (self, visit, arg, + nullptr, ObjWithAlloc_traverse_impl); +} + +/* The tp_clear callback for the A type. */ + +static void +ObjWithAlloc_clear_impl (PyObject *self) +{ + auto *obj = (ObjWithAlloc *) self; + printf ("--> ObjWithAlloc_clear_impl\n"); + Py_CLEAR (obj->m_name); + Py_CLEAR (obj->self_cycle); + if (obj->msg != nullptr) + { + delete obj->msg; + obj->msg = nullptr; + } +} + +static void +ObjWithAlloc_clear (PyObject *self) +{ + return gdbpy_heap_type::tp_clear (self, nullptr, ObjWithAlloc_clear_impl); +} + +PyObject * +ObjWithAlloc_name (PyObject *self, PyObject *args) +{ + auto *obj = (ObjWithAlloc *) self; + Py_INCREF (obj->m_name); + return obj->m_name; +} + +PyObject * +ObjWithAlloc_whoami (PyObject *self, PyObject *args) +{ + auto *obj = (ObjWithAlloc *) self; + return PyUnicode_FromFormat ("Hello I am %U", obj->m_name); +} + +PyObject * +ObjWithAlloc_speak (PyObject *self, PyObject *args) +{ + auto *obj = (ObjWithAlloc *) self; + return PyUnicode_FromFormat ("I, %U, %s", obj->m_name, obj->msg->c_str ()); +} + +PyMethodDef ObjWithAlloc_object_methods [] = { + { "name", ObjWithAlloc_name, METH_NOARGS, + "name () -> String.\n\ +Return the name of the class as a string value." }, + { "whoami", ObjWithAlloc_whoami, METH_NOARGS, + "whoami () -> String.\n\ +Return the object's greeting as a string value." }, + { "speak", ObjWithAlloc_speak, METH_NOARGS, + "speak () -> String.\n\ +Return the object's speech as a string value." }, + {nullptr} /* Sentinel */ +}; + +PyType_Slot ObjWithAlloc_object_type_slot[] = { + {Py_tp_doc, const_cast("ObjWithAlloc object")}, + gdbpy_heap_type_methods (ObjWithAlloc_traverse, ObjWithAlloc_clear), + {Py_tp_alloc, (void*) ObjWithAlloc_alloc}, + {Py_tp_methods, ObjWithAlloc_object_methods}, + {0, nullptr}, /* Sentinel */ +}; + +PyType_Spec ObjWithAlloc_object_type_spec = { + "MyModule.ObjWithAlloc", /* tp_name */ + sizeof (ObjWithAlloc), /* tp_basicsize */ + 0, /* tp_itemsize */ + gdbpy_heap_type::tp_flags_default () + | Py_TPFLAGS_BASETYPE, /* tp_flags */ + ObjWithAlloc_object_type_slot, +}; + +} // namespace anonymous + +gdbpy_object_type_ref +ObjWithAlloc::record_class + (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class ATTRIBUTE_UNUSED) +{ + gdbpy_object_type_ref obj_type + = gdbpy_type_initialize (module, + &ObjWithAlloc_object_type_spec); + + ObjWithAlloc_object_type = obj_type; + return obj_type; +} diff --git a/src/gdb-others.cpp b/src/gdb-others.cpp new file mode 100644 index 0000000..10e51d1 --- /dev/null +++ b/src/gdb-others.cpp @@ -0,0 +1,31 @@ +#include "gdb-others.hpp" + +#define TOOLNAME "GDB" + +#include +#include +#include + +extern "C" { + +[[noreturn]] static void +internal_verror (const char *file, int line, const char *fmt, va_list args) +{ + fprintf (stderr, "\ +%s:%d: A problem internal to " TOOLNAME " has been detected.\n", file, line); + vfprintf (stderr, fmt, args); + fprintf (stderr, "\n"); + abort (); +} + +void +internal_error_loc (const char *file, int line, const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + internal_verror (file, line, fmt, ap); + va_end (ap); +} + +} diff --git a/src/my_py_module.cpp b/src/my_py_module.cpp new file mode 100644 index 0000000..edb62cc --- /dev/null +++ b/src/my_py_module.cpp @@ -0,0 +1,29 @@ +#include + +#include "my_py_module.hpp" + +namespace { + +// A struct contains the definition of a module +PyModuleDef my_module = { + PyModuleDef_HEAD_INIT, + "MyModule", + "This is MyModule's docstring", + -1, // Optional size of the module state memory + NULL, // Optional module methods + NULL, // Optional slot definitions + NULL, // Optional traversal function + NULL, // Optional clear function + NULL // Optional module deallocation function +}; + +} // namespace anonymous + +PyMODINIT_FUNC +PyInit_MyModule(void) { + gdbpy_borrowed_ref<> module = PyModule_Create(&my_module); + auto A_object_type = A::record_class (module); + auto B_object_type = B::record_class (module, A_object_type); + auto ObjWithAlloc_object_type = ObjWithAlloc::record_class (module); + return module; +} diff --git a/src/my_py_module.hpp b/src/my_py_module.hpp new file mode 100644 index 0000000..6fc173f --- /dev/null +++ b/src/my_py_module.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "python-internal.hpp" + +#include +#include + +struct A: public PyObject +{ + /* Dictionary holding user-added attributes. + This is the __dict__ attribute of the object. */ + GDBPYOBJECT_DICT_ATTR (A); + + PyObject *m_a; + + static gdbpy_object_type_ref + record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class = nullptr); +}; + +static_assert (gdb::is_python_allocatable_v); + +struct B: public A +{ + PyObject *m_b; + + static gdbpy_object_type_ref + record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class = nullptr); +}; + +static_assert (gdb::is_python_allocatable_v); + +struct ObjWithAlloc: public PyObject +{ + const std::string *msg; + PyObject *m_name; + PyObject *self_cycle; + + static gdbpy_object_type_ref + record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class = nullptr); +}; + +static_assert (gdb::is_python_allocatable_v); diff --git a/src/py-obj-type.cpp b/src/py-obj-type.cpp new file mode 100644 index 0000000..6add108 --- /dev/null +++ b/src/py-obj-type.cpp @@ -0,0 +1,80 @@ +/* Helpers related to Python object type + + 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 . */ + +#include "python-internal.hpp" +#include "py-obj-type.hpp" + +/* Return the type's fully qualified name from a PyTypeObject. */ +std::string +gdb_py_tp_name (PyTypeObject *py_type) noexcept +{ + auto pyobj_to_str = [](PyObject *name) -> std::string + { + const char *s = PyUnicode_AsUTF8AndSize (name, nullptr); + return (s == nullptr) ? "" : s; + }; + +#if PY_VERSION_HEX >= 0x030d0000 + /* Note: PyType_GetFullyQualifiedName() was added in version 3.13, and is + part of the stable ABI since version 3.13. */ + gdbpy_ref<> fully_qualified_name (PyType_GetFullyQualifiedName (py_type)); + if (fully_qualified_name == nullptr) + return {}; + + return pyobj_to_str (fully_qualified_name.get ()); + +#else /* PY_VERSION_HEX < 0x030d0000 && ! defined (Py_LIMITED_API) */ + /* For non-heap types, the fully qualified name corresponds to tp_name. */ + if (! (PyType_GetFlags (py_type) & Py_TPFLAGS_HEAPTYPE)) + return py_type->tp_name; + + /* In the absence of PyType_GetFullyQualifiedName(), we fallback using + __qualname__ instead. However, the result may differ slightly in some + cases, e.g. the module name may be missing. */ + +# if PY_VERSION_HEX >= 0x030b0000 + /* Note: PyType_GetQualName() was added in version 3.11. */ + gdbpy_ref<> qualname (PyType_GetQualName (py_type)); + if (qualname == nullptr) + return {}; + + return pyobj_to_str (qualname.get ()); + +# else + /* In the absence of PyType_GetQualName(), fallback on using PyHeapTypeObject + which is not part of the public API. + Tested on 3.10 which is the oldest supported version at the time of this + writing, i.e. February 2026. Hopefully, this workaround should go away + when the minimum supported Python version is increased above 3.10. */ + PyHeapTypeObject *ht = (PyHeapTypeObject *) py_type; + if (ht->ht_qualname == nullptr) + return {}; + + return pyobj_to_str (ht->ht_qualname); +# endif +#endif +} + +/* Return the type's fully qualified name from a PyObject. */ +std::string +gdbpy_py_obj_tp_name (PyObject *self) noexcept +{ + /* Note: Py_TYPE () is part of the stable ABI since version 3.14. */ + return gdb_py_tp_name (Py_TYPE (self)); +} diff --git a/src/python-internal.cpp b/src/python-internal.cpp new file mode 100644 index 0000000..6b1a262 --- /dev/null +++ b/src/python-internal.cpp @@ -0,0 +1,87 @@ +#include "python-internal.hpp" +#include + +namespace { + +/* Like PyModule_AddObject, but does not steal a reference to + OBJECT. */ + +bool +gdb_pymodule_addobject (PyObject *mod, const char *name, PyObject *object) +{ + Py_INCREF (object); + int result = PyModule_AddObject (mod, name, object); + if (result < 0) + Py_DECREF (object); + return result == 0; +} + +} // namespace anonymous + +/* A wrapper for all the initialization logic of the Python type. + - PyType_FromSpec or PyType_FromSpecWithBases initializes the opaque type + PyTypeObject (which is itself a PyObject) from the provided PyType_Spec. + - gdb_pymodule_addobject registers the type in the appropriate module. + If MOD is supplied, then the type is added to that module. If MOD + is not supplied, the type name (PyType_Spec.name field) must be of + the form "gdb.Mumble", and the type will be added to the gdb module. + Returns true on success, false on error. */ + +gdbpy_object_type_ref +gdbpy_type_initialize (gdbpy_borrowed_ref<> &mod, + PyType_Spec *spec, + PyObject *bases, + bool disallow_instantiation) +{ + gdbpy_ref<> res (bases == nullptr + ? PyType_FromSpec (spec) + : PyType_FromSpecWithBases (spec, bases)); + if (res == nullptr) + return nullptr; + + gdbpy_object_type_ref object_type (std::move (res)); + + if (object_type.same_as (nullptr)) + { + PyErr_Print (); + return nullptr; + } + +#if PY_VERSION_HEX < 0x030a0000 + /* When using the Python unlimited API, disallowing the creation of instances + of the type in Python is done by setting tp_free to NULL. + When using the Python limited API, PyType_FromSpec and its siblings + automatically set tp_new to PyType_GenericNew(), which has for consequence + of allowing the creation of instances of the type in Python. + To disallow this behavior with the limited API, a new flag was introduced + in Python 3.9: Py_TPFLAGS_DISALLOW_INSTANTIATION. + For Python versions older than 3.9, we need to set explicitly tp_free to + NULL once the type object was created. This workaround is only available + when the limited API is disabled. */ +#if defined (Py_LIMITED_API) + #error "The version of Python you are using does not expose " \ + "Py_TPFLAGS_DISALLOW_INSTANTIATION as a part of the limited C API." +#else + if (disallow_instantiation) + object_type.pytype ()->tp_new = nullptr; + + /* PyType_Ready() is already called at the end of PyType_FromMetaclass() + (called by both PyType_FromSpec and PyType_FromSpecWithBases), so no + need to call it again unless the Python version does not have the flag + Py_TPFLAGS_DISALLOW_INSTANTIATION. In this case, a second call after + setting tp_new to NULL will disallow the instantiation for the type as + it used to be the case with non-heap types. */ + if (PyType_Ready (object_type) < 0) + return nullptr; +#endif +#else + (void) disallow_instantiation; +#endif + + const char *dot = strrchr (spec->name, '.'); + gdb_assert (dot != nullptr); + if (gdb_pymodule_addobject (mod.get (), dot + 1, object_type.get ()) < 0) + return nullptr; + + return object_type; +} +