Python C-API:PyDict_GetItem 上的分段错误,可能的参考问题?

Posted

技术标签:

【中文标题】Python C-API:PyDict_GetItem 上的分段错误,可能的参考问题?【英文标题】:Python C-API: segmentation fault on PyDict_GetItem, possible reference problem? 【发布时间】:2018-12-18 01:20:51 【问题描述】:

我在 C 中引用了一个 Python 字典列表。我正在编写一个函数来计算列表中两个成员之间的点积:

PyObject *handle; // reference to a list of dictionaries
virtual float dot_product () (unsigned i, unsigned j) const 
    // dot product of handle[i] and handle[j]
    PyObject *a = (PyObject*)PyList_GetItem(handle, (Py_ssize_t)i);
    PyObject *b = (PyObject*)PyList_GetItem(handle, (Py_ssize_t)j);
    PyObject *key, *a_value;
    Py_ssize_t pos = 0;
    double dot_product = 0;
    while (PyDict_Next(a, &pos, &key, &a_value)) 
        PyObject* b_value = PyDict_GetItem(b, key);
        if (b_value != NULL)
            dot_product += PyFloat_AsDouble(a_value) * PyFloat_AsDouble(b_value);
        
    
    return dot_product;

这会导致分段错误。使用 gdb 进行调试,看来分段错误是由 PyDict_GetItem(b, key) 引起的。这让我怀疑我的引用计数有问题。

阅读Reference Counts 上的文档后,似乎上面代码中的所有引用都是借用的,所以我认为没有必要使用 Py_INCREF 或 Py_DECREF ......但我很容易出错。上述代码中是否有需要使用 Py_INCREF 或 Py_DECREF 的地方?

编辑:我应该注意我已经进行了检查以确保 a 和 b 不为空,并且还检查以确保 i 和 j 不超过列表的大小。我从问题中的代码中删除了这些检查以使其更简单。 ——

【问题讨论】:

这个程序中是否有任何线程? dicts Python 的键是内置的,还是一些自定义类?通过不持有拥有的引用,您的密钥的 __hash____eq__ 方法可能会直接更改您正在使用的 lists 或 dicts,或者调用允许 GIL 切换的 Python 代码,允许其他一些线程开始运行并做同样的事情。 除此之外,您没有检查返回值;如果j 大于存储在handle 中的list 的长度,ab 很容易成为NULL,如果你不检查返回值,你最终会尝试尊重空指针。 PyDict_GetItem 抑制丢失键的异常,但它不会尝试防范 dict 本身的 NULL 值。 dict的键是整数,所以Python内置。您可能会使用线程:包含程序是多线程的,因此可以同时调用 dot_product。我将尝试一个单线程版本,看看是否能解决问题。 这还取决于您的 dot_product() 调用是否直接包装在 Python 调用下。 Python 函数调用会为您隐式增加引用计数,但如果您正在从 C++ 主动调用 dot_product,则需要自己执行此操作。 您使用的是 Python 3.10 或更高版本吗?在早期版本中,PyDict_GetItem 不检查 GIL,这可能会导致问题。 【参考方案1】:

检查您的返回值。如果ij 分别超过list 引用的list 的长度handle,则ab 都可以是NULLPyDict_GetItem 假设传递的 dict 不是 NULL 指针,在不确认该假设的情况下取消引用它,这将导致立即出现段错误。

您的主要问题是确定如何报告错误。 C++ 异常适用于 C++,但 Python 无法理解它,除非您捕获它并将其转换为 Python 级别的异常。无论如何,在你弄清楚之前,返回NaN 表示失败:

#include <cmath>

PyObject *handle; // reference to a list of dictionaries
virtual float dot_product () (unsigned i, unsigned j) const 
    // dot product of handle[i] and handle[j]
    PyObject *key, *a_value;
    Py_ssize_t pos = 0;
    double dot_product = 0;

    // Check both indices are valid
    PyObject *a = (PyObject*)PyList_GetItem(handle, (Py_ssize_t)i);
    if (!a) return NAN;

    PyObject *b = (PyObject*)PyList_GetItem(handle, (Py_ssize_t)j);
    if (!b) return NAN;

    // Test if you actually got dicts
    if (!PyDict_Check(a) || !PyDict_Check(b)) return NAN;

    while (PyDict_Next(a, &pos, &key, &a_value)) 
        PyObject* b_value = PyDict_GetItem(b, key);
        if (b_value != NULL)
            // Check that both values are really Python floats and extract C double
            double a_val = PyFloat_AsDouble(a_value);
            if (a_val == -1.0 && PyErr_Occurred()) return NAN;

            double b_val = PyFloat_AsDouble(b_value);
            if (b_val == -1.0 && PyErr_Occurred()) return NAN;

            dot_product += a_val * b_val;
        
    
    return dot_product;

【讨论】:

我应该在我的问题中这么说,但我已经完成了空值检查并确保 i 和 j 不超过列表的大小。我从问题中的代码中删除了检查以使其更简单。

以上是关于Python C-API:PyDict_GetItem 上的分段错误,可能的参考问题?的主要内容,如果未能解决你的问题,请参考以下文章

来自 Python 的 C-API - 如何获取字符串?

如何在 Python C-API 中动态创建派生类型

Python C-API:PyDict_GetItem 上的分段错误,可能的参考问题?

Python C-API 和 Numpy:import_array 上的核心转储

在 ubuntu 20.04 LTS 上使用 python Xlib 或 C-API 操作第二个(辅助)鼠标输入

C-API:分配“PyTypeObject-extension”