6

我有一些调用 Python 函数的 C 代码。这个 Python 函数接受一个地址并使用 WINFUNCTYPE 最终将其转换为 Python 可以调用的函数。作为参数发送给 Python 函数的 C 函数最终将调用另一个 Python 函数。正是在这最后一步导致崩溃。所以简而言之,我从 C -> Python -> C -> Python 开始。最后一个 C -> Python 导致崩溃。我一直试图理解这个问题,但我一直无法理解。

有人可以指出我的问题吗?

使用 Visual Studio 2010 编译并使用参数“c:\...\crash.py”和“func1”运行的 C 代码:

#include <stdlib.h>
#include <stdio.h>

#include <Python.h>

PyObject* py_lib_mod_dict; //borrowed

void __stdcall cfunc1()
{
    PyObject* py_func;
    PyObject* py_ret;
    int size;
    PyGILState_STATE gil_state;

    gil_state = PyGILState_Ensure();
    printf("Hello from cfunc1!\n");

    size = PyDict_Size(py_lib_mod_dict);
    printf("The dictionary has %d items!\n", size);
    printf("Calling with GetItemString\n");
    py_func = PyDict_GetItemString(py_lib_mod_dict, "func2"); //fails here when cfunc1 is called via callback... will not even go to the next line!
    printf("Done with GetItemString\n");
    py_ret = PyObject_CallFunction(py_func, 0);

    if (py_ret)
    {
        printf("PyObject_CallFunction from cfunc1 was successful!\n");
        Py_DECREF(py_ret);
    }
    else
        printf("PyObject_CallFunction from cfunc1 failed!\n");

    printf("Goodbye from cfunc1!\n");
    PyGILState_Release(gil_state);
}

int wmain(int argc, wchar_t** argv)
{
    PyObject* py_imp_str;
    PyObject* py_imp_handle;
    PyObject* py_imp_dict; //borrowed
    PyObject* py_imp_load_source; //borrowed
    PyObject* py_dir; //stolen
    PyObject* py_lib_name; //stolen
    PyObject* py_args_tuple;
    PyObject* py_lib_mod;
    PyObject* py_func;
    PyObject* py_ret;

    Py_Initialize();

    //import our python script
    py_dir = PyUnicode_FromWideChar(argv[1], wcslen(argv[1]));
    py_imp_str = PyString_FromString("imp");
    py_imp_handle = PyImport_Import(py_imp_str);
    py_imp_dict = PyModule_GetDict(py_imp_handle); //borrowed
    py_imp_load_source = PyDict_GetItemString(py_imp_dict, "load_source"); //borrowed
    py_lib_name = PyUnicode_FromWideChar(argv[2], wcslen(argv[2]));

    py_args_tuple = PyTuple_New(2);
    PyTuple_SetItem(py_args_tuple, 0, py_lib_name); //stolen
    PyTuple_SetItem(py_args_tuple, 1, py_dir); //stolen

    py_lib_mod = PyObject_CallObject(py_imp_load_source, py_args_tuple);
    py_lib_mod_dict = PyModule_GetDict(py_lib_mod); //borrowed

    printf("Calling cfunc1 from main!\n");
    cfunc1();

    py_func = PyDict_GetItem(py_lib_mod_dict, py_lib_name);
    py_ret = PyObject_CallFunction(py_func, "(I)", &cfunc1);

    if (py_ret)
    {
        printf("PyObject_CallFunction from wmain was successful!\n");
        Py_DECREF(py_ret);
    }
    else
        printf("PyObject_CallFunction from wmain failed!\n");

    Py_DECREF(py_imp_str);
    Py_DECREF(py_imp_handle);
    Py_DECREF(py_args_tuple);
    Py_DECREF(py_lib_mod);

    Py_Finalize();

    fflush(stderr);
    fflush(stdout);
    return 0;
}

Python代码:

from ctypes import *

def func1(cb):
    print "Hello from func1!"
    cb_proto = WINFUNCTYPE(None)
    print "C callback: " + hex(cb)
    call_me = cb_proto(cb)
    print "Calling callback from func1."
    call_me()
    print "Goodbye from func1!"

def func2():
    print "Hello and goodbye from func2!"

输出:

Calling cfunc1 from main!
Hello from cfunc1!
The dictionary has 88 items!
Calling with GetItemString
Done with GetItemString
Hello and goodbye from func2!
PyObject_CallFunction from cfunc1 was successful!
Goodbye from cfunc1!
Hello from func1!
C callback: 0x1051000
Calling callback from func1.
Hello from cfunc1!
The dictionary has 88 items!
Calling with GetItemString
PyObject_CallFunction from wmain failed!

我在末尾添加了 PyErr_Print() ,结果如下:

Traceback (most recent call last):
  File "C:\Programming\crash.py", line 9, in func1
    call_me()
WindowsError: exception: access violation writing 0x0000000C

编辑:修复了 abarnert 指出的错误。输出不受影响。
编辑:在解决错误的代码中添加(在 cfunc1 中获取 GIL 锁)。再次感谢阿巴纳特。

4

2 回答 2

5

问题是这段代码:

py_func = PyDict_GetItemString(py_lib_mod_dict, "func2"); //fails here when cfunc1 is called via callback... will not even go to the next line!
printf("Done with GetItemString\n");
py_ret = PyObject_CallFunction(py_func, 0);

Py_DECREF(py_func);

正如文档所说,PyDict_GetItemString返回一个借来的参考。所以,你第一次在这里调用时,你借用了引用,然后去引用它,导致它被销毁。下次你打电话时,你会得到垃圾,并尝试调用它。

因此,要修复它,只需删除Py_DECREF(py_func)(或在行Py_INCREF(py_func)后添加pyfunc =)。

实际上,你通常会得到一个特殊的“死”对象,所以你可以很容易地测试它:在行之后和行之后PyObject_Print(py_func, stdout),你可能会看到第一次、第二次和第三次(你不会看到第四个,因为它会在你到达那里之前崩溃)。py_func =Py_DECREF<function func2 at 0x10b9f1230><refcnt 0 at 0x10b9f1230>

我没有方便的 Windows 框,但是将wmain, wchar_t, PyUnicode_FromWideChar,WINFUNCTYPE等更改为main, char, PyString_FromString,CFUNCTYPE等,我能够构建和运行您的代码,但我在同一个地方崩溃了......并且修复工作.

还有……你不应该把 GIL 放在里面cfunc1吗?我不经常写这样的代码,所以也许我错了。而且我不会因为代码原样而崩溃。显然,产生一个线程来运行cfunc1 确实会崩溃,并且PyGILState_Ensure/Release解决了该崩溃……但这并不能证明您在单线程情况下需要任何东西。所以也许这无关紧要……但如果你在修复第一个崩溃后再次崩溃(在线程情况下,我的看起来像Fatal Python error: PyEval_SaveThread: NULL tstate),请调查一下。

顺便说一句,如果您是 Python 扩展和嵌入的新手:大量无法解释的崩溃,就像这个一样,是由手动引用计数错误引起的。这就是诸如boost::python等之类的东西存在的原因。并不是说用普通的 C API 就不可能把它弄好,只是它很容易出错,而且你必须习惯于调试这样的问题。

于 2013-02-03T23:39:08.170 回答
2

abarnert 的回答提供了正确的调用函数,但解释让我感到困扰,所以我早早回家并四处寻找。

在开始解释之前,我想提一下,当我说 GIL 时,我严格指的是互斥锁、信号量或 Global Interpreter Lock 用来进行线程同步的任何东西。这不包括 Python 在获取和释放 GIL 之前/之后所做的任何其他内务处理。

单线程程序不会初始化 GIL,因为您从不调用 PyEval_InitThreads()。因此没有 GIL。即使发生了锁定,也没关系,因为它是单线程的。但是,获取和释放 GIL 的函数除了获取/释放 GIL 之外,还会做一些有趣的事情,比如弄乱线程状态。关于 WINFUNCTYPE 对象的文档明确指出它在跳转到 C 之前释放 GIL。因此,当在 Python 中调用 C 回调时,我怀疑调用了 PyEval_SaveThread() 之类的东西(可能是错误的,因为它只是假设在线程中调用至少根据我的理解操作)。这将释放 GIL(如果存在)并将线程状态设置为 NULL,但是在单线程 Python 程序中没有 GIL,因此,它真正所做的只是将线程状态设置为 NULL。这会导致 C 回调中的大多数 Python 函数严重失败。

真正调用 PyGILState_Ensure/Release 的唯一好处是告诉 Python 在运行和执行操作之前将线程状态设置为有效的状态。没有要获取的 GIL(未初始化,因为我从未调用 PyEval_InitThreads())。

测试我的理论:在主函数中,我使用 PyThreadState_Swap(NULL) 来获取线程状态对象的副本。我在回调期间恢复它,一切正常。如果我将线程状态保持为 null,即使没有执行 Python -> C 回调,我也会遇到几乎相同的访问冲突。在 cfunc1 内部,我恢复了线程状态,并且在 Python -> C 回调期间 cfunc1 本身不再有问题。

当 cfunc1 返回 Python 代码时会出现问题,但这可能是因为我弄乱了线程状态,而 WINFUNCTYPE 对象期待完全不同的东西。如果您在返回时保持线程状态而不将其设置回 null,则 Python 只会坐在那里,什么也不做。如果将其恢复为 null,它会崩溃。但是,它确实成功执行了 cfunc1,所以我不确定我是否太在意。

我最终可能会在 Python 源代码中四处寻找以 100% 确定,但我确信足以满足。

于 2013-02-07T01:58:21.187 回答