从 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。所以 buffer
将 char *
到 1024 的内存库。很好。然后你 memset
buffer
到 [=19=]
1024 字节。美好的。那你做
result = recvfrom(RecvSocketDescriptor,
(char*)buffer,
MAXLINE,
flags,
(struct sockaddr*)&ClientAddr,
&socketLength);
理论上可以从套接字读取最大 1024 字节。返回 1024 到 result
和 buffer
设置为读取的 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)
在此之后,所有线程都在播放。
我正在 运行 从 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
如果可能,我想避免包含
.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。所以 buffer
将 char *
到 1024 的内存库。很好。然后你 memset
buffer
到 [=19=]
1024 字节。美好的。那你做
result = recvfrom(RecvSocketDescriptor,
(char*)buffer,
MAXLINE,
flags,
(struct sockaddr*)&ClientAddr,
&socketLength);
理论上可以从套接字读取最大 1024 字节。返回 1024 到 result
和 buffer
设置为读取的 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)
在此之后,所有线程都在播放。