在C中嵌入Python:尝试在Python代码调用的C回调中调用Python代码时出错

时间:2013-02-03 20:53:56

标签: python c python-2.7

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

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

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

#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锁)。再次感谢abarnert。

2 个答案:

答案 0 :(得分: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);

正如the docs所说,PyDict_GetItemString会返回借来的引用。因此,当你第一次在这里打电话时,你借用了引用,然后减去它,导致它被销毁。下次你打电话时,你会收回垃圾,然后试着打电话。

因此,要解决此问题,只需删除Py_DECREF(py_func)(或在Py_INCREF(py_func)行后添加pyfunc =)。

实际上,你通常会找回一个特殊的&#34;死的&#34;对象,因此您可以非常轻松地对此进行测试:在PyObject_Print(py_func, stdout)行之后和py_func =行之后放置Py_DECREF,您可能会看到<function func2 at 0x10b9f1230>之类的内容第一次,<refcnt 0 at 0x10b9f1230>第二次和第三次(你不会看到第四次,因为它会在你到达之前崩溃)。

我没有方便的Windows框,但将wmainwchar_tPyUnicode_FromWideCharWINFUNCTYPE等更改为maincharPyString_FromStringCFUNCTYPE等等,我能够构建并运行您的代码,并且我在同一个地方遇到了崩溃......并且修复工作正常。

另外......你不应该在cfunc1内举行GIL吗?我不经常写这样的代码,所以也许我错了。而且我没有按原样使用代码崩溃。显然,产生一个运行cfunc1 的线程会崩溃,而PyGILState_Ensure / Release会解决这个崩溃...但这并不能证明你需要任何东西单线程案例。所以也许这不相关......但是如果你在修复第一个崩溃后再次崩溃(在线程的情况下,我看起来像Fatal Python error: PyEval_SaveThread: NULL tstate),请看看。

顺便说一句,如果您是Python扩展和嵌入的新手:大量无法解释的崩溃,就像这一次,是由手动引用计数错误引起的。这就是boost::python等等存在的原因。并不是说用普通的C API做到这一点是不可能的,只是它很容易弄错,你将不得不习惯于调试这样的问题。

答案 1 :(得分:2)

abarnert的答案提供了正确的调用功能,但是这个解释让我感到困扰所以我早早回家并且更多地探索了。

在我进入解释之前,我想提一下,当我说GIL时,我严格意味着互斥体,信号量或全局解释器锁用于进行线程同步的任何东西。这不包括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())。

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

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

我最终可能会在Python源代码中找到100%肯定,但我确信会满意。