加载PyQt等第三方模块后,使用ctypesgen生成绑定的含std::recursive_mutex的C++库触发Python崩溃问题
我在Windows 10环境下,尝试从Python脚本调用C++库入口点时遇到了奇怪的崩溃,已经把问题简化成了极简示例,具体情况如下:
我的C++代码实现
sdetests_lib_bind_python.h
#pragma once #ifdef SDETESTS_LIB_BIND_PYTHON_EXPORTS /** Export symbols statement */ #define SDETESTS_LIB_BIND_PYTHON __declspec(dllexport) #else /** Import symbols statement */ #define SDETESTS_LIB_BIND_PYTHON __declspec(dllimport) #endif #ifdef __cplusplus extern "C" { #endif SDETESTS_LIB_BIND_PYTHON void func(int lock); #ifdef __cplusplus } #endif
sdetests_lib_bind_python.cpp
#include "sdetests_lib_bind_python.h" #include <iostream> #include <mutex> SDETESTS_LIB_BIND_PYTHON void func( int lock ) { if (lock!=0) { std::cout << "Creating mutex" << std::endl; std::recursive_mutex mutex; std::cout << "Created mutex" << std::endl; std::cout << "Locking mutex" << std::endl; std::lock_guard<std::recursive_mutex> lock_guard(mutex); std::cout << "Locked mutex" << std::endl; } std::cout << "Hello world" << std::endl; }
我用Visual Studio 2022(MSVC 19.43.34808.0)将其编译为共享库,随后用ctypesgen生成绑定文件sdetests_lib_bind_python.py,用以下简单Python脚本调用时完全正常:
import sdetests_lib_bind_python print("Calling func without lock") sdetests_lib_bind_python.func(0) print("Calling func with lock") sdetests_lib_bind_python.func(1)
输出结果:
Calling func without lock Hello world Calling func with lock Creating mutex Created mutex Locking mutex Locked mutex Hello world
但如果先加载PyQt模块,脚本修改为:
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR print("Qt: v", QT_VERSION_STR, "\tPyQt: v", PYQT_VERSION_STR) import sdetests_lib_bind_python print("Calling func without lock") sdetests_lib_bind_python.func(0) print("Calling func with lock") sdetests_lib_bind_python.func(1)
程序就会崩溃,输出:
Qt: v 5.15.2 PyQt: v 5.15.9 Calling func without lock Hello world Calling func with lock Creating mutex Created mutex Locking mutex Traceback (most recent call last): File "example_sdetests_lib_bind_python_def.py", line 10, in <module> sdetests_lib_bind_python.func(1) OSError: exception: access violation reading 0x0000000000000000
问题特征与排查细节
这个崩溃有几个关键规律:
- 仅当PyQt(或rclpy这类其他模块)先于我的C++库绑定被导入时触发;若把
import sdetests_lib_bind_python放在脚本第一行,后续加载其他模块完全正常。 - 旧版本编译器(MSVC 19.38.33135.0)编译时无此问题,是升级编译器后才出现的。
- 仅当库中使用
std::recursive_mutex时崩溃;仅包含<mutex>头文件无影响,但创建并锁定mutex会触发访问违例。 - Debug模式编译的库不会出现崩溃。
我需要给外部用户提供该库,要求他们必须先导入我的库或升级第三方模块并不现实,因此急需找到根本解决办法。
根据建议,我检查了库加载顺序,当PyQt5在我的库之后加载(无崩溃)时,加载顺序片段如下:
'python.exe' (Win32) : Chargé 'C:\Python38\python.exe'. Les symboles ont été chargés. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\ntdll.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\kernel32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\KernelBase.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\ucrtbase.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\python38.dll'. Les symboles ont été chargés. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\shlwapi.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\msvcrt.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\ws2_32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\rpcrt4.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\advapi32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\sechost.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\bcrypt.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\vcruntime140.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\version.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. Le thread 27884 s'est arrêté avec le code 0 (0x0). 'python.exe' (Win32) : Chargé 'C:\Windows\System32\cryptsp.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\rsaenh.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\cryptbase.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\bcryptprimitives.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\python3.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\DLLs\_ctypes.pyd'. Les symboles ont été chargés. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\ole32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\DLLs\libffi-7.dll'. Le module a été généré sans symboles. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\combase.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\gdi32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\win32u.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\gdi32full.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\msvcp_win.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\user32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\oleaut32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\imm32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\DLLs\_bz2.pyd'. Les symboles ont été chargés. 'python.exe' (Win32) : Chargé 'C:\Python38\DLLs\_lzma.pyd'. Les symboles ont été chargés. 'python.exe' (Win32) : Chargé 'C:\dev\vobs_sde\build\sdetests_python_binding\win64\stg\Release\sdetests_lib_bind_python.dll'. Le module a été généré sans symboles. 'python.exe' (Win32) : Chargé 'C:\Python38\vcruntime140_1.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\msvcp140.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\Lib\site-packages\PyQt5\QtCore.pyd'. Le module a été généré sans symboles. 'python.exe' (Win32) : Chargé 'C:\Python38\Lib\site-packages\PyQt5\Qt5\bin\Qt5Core.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\shell32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\mpr.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\netapi32.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Windows\System32\userenv.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude. 'python.exe' (Win32) : Chargé 'C:\Python38\Lib\site-packages\PyQt5\Qt5\bin\msvcp140_1.dll'. Chargement des symboles désactivé par le paramètre Include/Exclude.
可能的原因与解决思路
看起来这个问题大概率是MSVC运行时库的版本冲突或加载顺序导致的。新版本MSVC对标准库组件的实现做了调整,而PyQt这类第三方模块可能自带旧版本的MSVC运行时库文件,当它们先被加载时,你的C++库会绑定到这些旧版本的运行时,而std::recursive_mutex的实现在新旧版本间不兼容,最终触发访问违例。
给你几个可以尝试的解决方向:
强制库使用静态链接的MSVC运行时
在Visual Studio项目设置里,把C/C++ -> 代码生成 -> 运行库改成多线程(/MT)(Release模式)或多线程调试(/MTd)(Debug模式)。这样你的库会把运行时库打包到自身,不会和其他模块的运行时冲突。不过静态链接会增大库体积,若你的库还要和其他动态链接组件交互,需额外考虑兼容性。替换
std::recursive_mutex为Windows原生互斥量
既然问题出在标准库的mutex上,可以用Windows API的CreateMutexW、WaitForSingleObject、ReleaseMutex实现递归互斥逻辑,绕开标准库的实现:#include <windows.h> SDETESTS_LIB_BIND_PYTHON void func( int lock ) { if (lock!=0) { std::cout << "Creating mutex" << std::endl; HANDLE mutex = CreateMutexW(NULL, FALSE, NULL); if (mutex == NULL) { std::cerr << "Failed to create mutex" << std::endl; return; } std::cout << "Created mutex" << std::endl; std::cout << "Locking mutex" << std::endl; DWORD waitResult = WaitForSingleObject(mutex, INFINITE); if (waitResult != WAIT_OBJECT_0) { std::cerr << "Failed to lock mutex" << std::endl; CloseHandle(mutex); return; } std::cout << "Locked mutex" << std::endl; // 原有业务逻辑 ReleaseMutex(mutex); CloseHandle(mutex); } std::cout << "Hello world" << std::endl; }这样完全不依赖标准库的mutex实现,也就不会有版本冲突问题。
显式指定运行时库的加载优先级(应急方案)
可以在Python脚本里先显式加载你库依赖的MSVC运行时DLL,再加载其他模块,但这个方法比较hack,需确认具体依赖的运行时文件:import ctypes # 加载库依赖的运行时DLL ctypes.CDLL("vcruntime140_1.dll") ctypes.CDLL("msvcp140.dll") # 再加载PyQt等模块 from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR print("Qt: v", QT_VERSION_STR, "\tPyQt: v", PYQT_VERSION_STR) import sdetests_lib_bind_python # 后续调用逻辑
另外你提到Debug模式没问题,是因为Debug模式的运行时库(如vcruntime140d.dll)和Release模式是分开的,PyQt这类模块一般不会携带Debug版本的运行时,所以不会冲突。
优先试试静态链接运行时的方案,对用户完全透明,不需要他们做任何额外操作。
备注:内容来源于stack exchange,提问作者jpo38

