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 ロックを取得)。abarnert さん、ありがとうございます。

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。そこに到達する前にクラッシュするため、4 番目は表示されません)。<function func2 at 0x10b9f1230><refcnt 0 at 0x10b9f1230>

便利な Windows ボックスはありませんが、、、、、などを 、、、などに変更すると、コードをwmainビルドwchar_tして実行することができ、同じ場所でクラッシュが発生し、修正が機能しました.PyUnicode_FromWideCharWINFUNCTYPEmaincharPyString_FromStringCFUNCTYPE

あと…GILを中に入れてはいけませんcfunc1か?私はこのようなコードを書くことはあまりないので、間違っているかもしれません。そして、そのままのコードでクラッシュすることはありません。明らかに、実行するスレッドを生成するとクラッシュcfunc1 PyGILState_Ensure、そのクラッシュを解決しますRelease...しかし、シングルスレッドの場合は何も必要ないという証明にはなりません。したがって、これは関係ないかもしれません…しかし、最初のものを修正した後に別のクラッシュが発生した場合 (スレッド化されたケースでは、私の場合は のようFatal Python error: PyEval_SaveThread: NULL tstateに見えました)、これを調べてください。

ところで、Python の拡張と埋め込みに慣れていない場合: 今回のような説明のつかないクラッシュの多くは、手動の refcounting エラーが原因です。それがboost::python、などのようなものが存在する理由です。単純な C API で正しく行うことが不可能というわけではありませんが、間違ってしまうのは非常に簡単であり、このような問題のデバッグに慣れる必要があります。

于 2013-02-03T23:39:08.170 に答える
2

abarnert の答えは、呼び出す正しい関数を提供しましたが、説明が気になったので、早く家に帰って、もう少し突っ込んでみました。

説明に入る前に、私が GIL と言うとき、厳密にはミューテックス、セマフォ、またはグローバル インタープリター ロックがスレッド同期を行うために使用するものすべてを意味していることに言及したいと思います。これには、Python が GIL を取得して解放する前後に行うその他のハウスキーピングは含まれません。

PyEval_InitThreads() を呼び出すことがないため、シングル スレッド プログラムは GIL を初期化しません。したがって、GIL はありません。ロックが発生していたとしても、シングル スレッドであるため問題にはなりません。ただし、GIL を取得および解放する関数は、GIL を取得/解放するだけでなく、スレッドの状態をいじるなどの面白いことも行います。WINFUNCTYPE オブジェクトのドキュメントには、C へのジャンプを行う前に GIL を解放することが明示的に記載されています。そのため、C コールバックが Python で呼び出されたとき、PyEval_SaveThread() のようなものが呼び出されていると思われます (スレッドでのみ呼び出されることを想定しているため、エラーである可能性があります)。少なくとも私の理解では操作)。これにより、GIL が解放され (存在する場合)、スレッドの状態が NULL に設定されますが、シングル スレッドの Python プログラムには GIL がありません。したがって、スレッドの状態を NULL に設定するだけです。これにより、C コールバックの Python 関数の大部分が激しく失敗します。

実際、PyGILState_Ensure/Release を呼び出す唯一の利点は、実行して何かを実行する前に、スレッドの状態を有効なものに設定するよう Python に指示することです。取得する GIL はありません (PyEval_InitThreads() を呼び出したことがないため、初期化されていません)。

私の理論をテストするには: main 関数で、PyThreadState_Swap(NULL) を使用してスレッド状態オブジェクトのコピーを取得します。コールバック中に復元すると、すべて正常に動作します。スレッドの状態を null のままにしておくと、Python -> C コールバックを実行しなくても、ほぼ同じアクセス違反が発生します。cfunc1 内でスレッド状態を復元すると、Python -> C コールバック中に cfunc1 自体に問題はなくなりました。

cfunc1 が Python コードに戻るときに問題がありますが、これはおそらく、スレッドの状態をいじり、WINFUNCTYPE オブジェクトがまったく異なるものを期待しているためです。戻るときにスレッドを null に戻さずに状態を維持すると、Python はただそこに座って何もしません。null に戻すと、クラッシュします。ただし、 cfunc1 は正常に実行されるため、あまり気にしすぎているかどうかはわかりません。

最終的には 100% 確実に Python のソース コードを調べてみるかもしれませんが、十分に満足できると確信しています。

于 2013-02-07T01:58:21.187 に答える