从 c/cpp 库调用 python 回调时出现段错误 11

Segfault 11 when invoking python callback from c/cpp library

我正在 运行 从 c/cpp 库中创建一个 UDP 套接字,并从 python 传递回调。

回调 运行 很好,直到我尝试修改 python 应用程序的成员变量。当我尝试修改成员变量时,我在任意时间后收到段错误 11。

我很好奇这是否意味着我需要通过将回调调用包装在 py_BEGIN_ALLOW_THREADS 和 py_END_ALLOW_THREADS 中来处理 GIL:https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock

如果可能,我想避免包含 ,因为这是一个抽象库,旨在与 .net

兼容

.cpp回调定义

#ifdef _WIN32
typedef void(__stdcall* UDPReceive)(const char* str);
#else
typedef void (*UDPReceive)(const char* str);
#endif

.cpp线程启动

ReceiveThread = std::async(std::launch::async, &MLFUDP::ReceivePoller, this, callback);

.h ReceiveCallback

UDPReceive ReceiveCallback = nullptr;

.cpp 接收触发 python 回调的线程

void UDP::ReceivePoller(UDPReceive callback)
{
    ReceiveCallback = callback
    ReceiverRunning = true;

    UDPLock *receiveLock = new UDPLock();

#ifdef _WIN32
    int socketLength = sizeof(ClientAddr);
    int flags = 0;
#else
    socklen_t socketLength = sizeof(ClientAddr);
    int flags = MSG_WAITALL;
#endif

    int result;
    char buffer[MAXLINE];
    while(ReceiverRunning)
    {
        try {
            memset(buffer,'[=15=]', MAXLINE);
            result = recvfrom(RecvSocketDescriptor,
                              (char*)buffer,
                              MAXLINE,
                              flags,
                              (struct sockaddr*)&ClientAddr,
                              &socketLength);
#ifdef _WIN32
            if (result == SOCKET_ERROR)
            {
                Log::LogErr("UDP Received error: " + std::to_string(WSAGetLastError()));
            }
#else
            if(result < 0)
            {
                Log::LogErr("UDD Received error: " + std::to_string(result));
            }
#endif
            buffer[result] = '[=15=]';

#ifdef _WIN32
            char* data = _strdup(buffer);
#else
            char* data = strdup(buffer);
#endif
            //handle overlfow
            if(data == nullptr) {continue;}
            receiveLock->Lock();
            //Fire Callback
            ReceiveCallback(data); 
            receiveLock->Unlock();

        }
        catch(...)
        {
            //okay, we want graceful exit when killing socket on close
        }
    }

}

**.py 库初始化**

    def __init__(self, udp_recv_port, udp_send_port):
        libname = ""
        if platform == "win32":
            print("On Windows")
            libname = pathlib.Path(__file__).resolve().parent / "SDK_WIN.dll"
        elif platform == "darwin":
            print("on Mac")
            libname = pathlib.Path(__file__).resolve().parent / "SDK.dylib"
            print(libname)
        elif platform == "linux":
            print("on linux")

        UDP_TYPE_BIND = 0

        #Load dynamic library
        self.sdk = CDLL(str(libname))

        callback_type = CFUNCTYPE(None, c_char_p)
        log_callback = callback_type(sdk_log_function)
        self.sdk.InitLogging(2, log_callback)

        recv_callback = callback_type(self.sdk_recv_callback)
        self.sdk.InitUDP(udp_recv_port, udp_send_port, UDP_TYPE_BIND, recv_callback)

.py recv_callback 定义 如果我 运行 这个回调一切正常,已经用几百万条消息向它发送了垃圾邮件

    @staticmethod
    def sdk_recv_callback(message):
        print(message.decode('utf-8'))
        string_data = str(message.decode('utf-8'));
        if len(string_data) < 1:
            print("Returning")
            return

然而,如果我随后将此消息添加到线程安全的 FIFO queue.Queue() 我在接收消息的任意(短)时间后收到段错误 11

 @staticmethod
    def sdk_recv_callback(message):
        print(message.decode('utf-8'))
        string_data = str(message.decode('utf-8'));
        if len(string_data) < 1:
            print("Returning")
            return

        message_queue.put(string_data)

.py 轮询函数提取消息队列

    def process_messages(self):
        while self.is_running:
            string_message = message_queue.get();
            data = json.loads(string_message);
            print(data)

大部分我都是边学边学的(在筒仓里),所以我认为我很有可能遗漏了一些东西basic/fundamental。我将不胜感激任何关于更好的方法或只是另一双眼睛的建议。谢谢。

目前正在 macOS 上使用 cmake 在 m1 芯片上编译。

嗯嗯。复杂的这个。我唯一能想到的是 UDP::ReceivePoller 函数中的缓冲区溢出。你用 char buffer[MAXLINE]; 声明了一个 char *。例如说 MAXLINE 是 = 1024。所以 bufferchar * 到 1024 的内存库。很好。然后你 memset buffer[=19=] 1024 字节。美好的。那你做

result = recvfrom(RecvSocketDescriptor,
                  (char*)buffer,
                  MAXLINE,
                  flags,
                  (struct sockaddr*)&ClientAddr,
                  &socketLength);

理论上可以从套接字读取最大 1024 字节。返回 1024 到 resultbuffer 设置为读取的 1024 字节。然后你设置 buffer[result] = '[=22=]'; 将缓冲区的索引 1024 设置为空。但是,索引是从 0 而不是 1。因此,将 1024 保留后的 1 个字节设置为“\0”。而且我想没问题(因为它只有 1 个字节)一点点。最终 buffer 被放置在它不应该访问的内存中某处的东西旁边并且它分段。所以我的猜测是:

a) 将 recvfrom(...) 更新为仅就绪 MAXLINE - 1 字节。这样你只能从套接字中准备好 1023 个字节。将缓冲区中的第 1024 个字节保留为空。

b) 将 buffer 更新为 char buffer[MAXLINE + 1]; 以提供额外的 1 个字节...(记得将 memset 也更新为 MAXLINE+1)

根据以往使用 cython's with gil construct 的经验,您需要在回调 python 时获取 GIL。

python doc's 看来,您需要调用 PyGILState_Ensure() 来获取 GIL 并调用 PyGILState_Release() 来释放 GIL。

部分问题可能是因为您遇到段错误,所以很难获得有关错误发生位置的信息。

您可能希望 import faulthandler 并在程序开始时调用 faulthander.enable()(参见 https://docs.python.org/3/library/faulthandler.html#faulthandler.enable)。使用 faulthandler 可以提供一些关于段错误的最小堆栈跟踪信息,并帮助您找到问题。

事实证明我不需要在我的 c 库中使用 python.h 来处理 GIL。由于我使用的是 ctypes,它通过在每次调用回调时启动一个临时 python 线程来“神奇地”处理 GIL ()

这个段错误是因为 process_message 函数,我是 运行 来自一个线程。段错误是因为我从 class 内部初始化了 ctypes 库。相反,我在 main 上初始化 SDK 并传递了对 class

的引用
if __name__ == "__main__":

faulthandler.enable()
libname = ""
if platform == "win32":
    print("On Windows")
    libname = pathlib.Path(__file__).resolve().parent / "SDK_WIN.dll"
elif platform == "darwin":
    print("on Mac")
    libname = pathlib.Path(__file__).resolve().parent / "MLFSDK.dylib"
    print(libname)
elif platform == "linux":
    print("on linux")

sdk = CDLL(str(libname))

app = the_app(sdk,6666,7777)

在此之后,所有线程都在播放。