bpo-9263: Dump Python object on GC assertion failure (GH-10062)

Changes:

* Add _PyObject_AssertFailed() function.
* Add _PyObject_ASSERT() and _PyObject_ASSERT_WITH_MSG() macros.
* gc_decref(): replace assert() with _PyObject_ASSERT_WITH_MSG() to
  dump the faulty object if the assertion fails.

_PyObject_AssertFailed() calls:

* _PyMem_DumpTraceback(): try to log the traceback where the object
  memory has been allocated if tracemalloc is enabled.
* _PyObject_Dump(): log repr(obj).
* Py_FatalError(): log the current Python traceback.

_PyObject_AssertFailed() uses _PyObject_IsFreed() heuristic to check
if the object memory has been freed by a debug hook on Python memory
allocators.

Initial patch written by David Malcolm.

Co-Authored-By: David Malcolm <dmalcolm@redhat.com>
diff --git a/Include/object.h b/Include/object.h
index 8b2afc2..4a49609 100644
--- a/Include/object.h
+++ b/Include/object.h
@@ -1105,6 +1105,53 @@
 _PyObject_DebugTypeStats(FILE *out);
 #endif /* ifndef Py_LIMITED_API */
 
+
+#ifndef Py_LIMITED_API
+/* Define a pair of assertion macros:
+   _PyObject_ASSERT_WITH_MSG() and _PyObject_ASSERT().
+
+   These work like the regular C assert(), in that they will abort the
+   process with a message on stderr if the given condition fails to hold,
+   but compile away to nothing if NDEBUG is defined.
+
+   However, before aborting, Python will also try to call _PyObject_Dump() on
+   the given object.  This may be of use when investigating bugs in which a
+   particular object is corrupt (e.g. buggy a tp_visit method in an extension
+   module breaking the garbage collector), to help locate the broken objects.
+
+   The WITH_MSG variant allows you to supply an additional message that Python
+   will attempt to print to stderr, after the object dump. */
+#ifdef NDEBUG
+   /* No debugging: compile away the assertions: */
+#  define _PyObject_ASSERT_WITH_MSG(obj, expr, msg) ((void)0)
+#else
+   /* With debugging: generate checks: */
+#  define _PyObject_ASSERT_WITH_MSG(obj, expr, msg)     \
+     ((expr)                                           \
+      ? (void)(0)                                      \
+      : _PyObject_AssertFailed((obj),                  \
+                               (msg),                  \
+                               Py_STRINGIFY(expr),     \
+                               __FILE__,               \
+                               __LINE__,               \
+                               __func__))
+#endif
+
+#define _PyObject_ASSERT(obj, expr) _PyObject_ASSERT_WITH_MSG(obj, expr, NULL)
+
+/* Declare and define _PyObject_AssertFailed() even when NDEBUG is defined,
+   to avoid causing compiler/linker errors when building extensions without
+   NDEBUG against a Python built with NDEBUG defined. */
+PyAPI_FUNC(void) _PyObject_AssertFailed(
+    PyObject *obj,
+    const char *msg,
+    const char *expr,
+    const char *file,
+    int line,
+    const char *function);
+#endif /* ifndef Py_LIMITED_API */
+
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/Include/pymem.h b/Include/pymem.h
index e993628..19f0c8a 100644
--- a/Include/pymem.h
+++ b/Include/pymem.h
@@ -196,7 +196,7 @@
 
    The function does nothing if Python is not compiled is debug mode. */
 PyAPI_FUNC(void) PyMem_SetupDebugHooks(void);
-#endif
+#endif   /* Py_LIMITED_API */
 
 #ifdef Py_BUILD_CORE
 /* Set the memory allocator of the specified domain to the default.