如何在Windows系统的Python中捕获DLL的stdout/stderr输出?
在Windows系统的Python中捕获DLL的stdout/stderr输出
问题场景
比如以下代码会直接把"hello"打印到stderr,我们需要将其捕获为字符串而非直接输出:
import ctypes string = b'hello\n' ctypes.cdll.msvcrt._write(2, string, len(string))
已尝试的无效方法
- 临时替换
sys.stderr为StringIO(或使用contextlib.redirect_stdout):无法捕获输出,因为内容来自C库函数而非Python打印语句,Linux环境下同样无效。 - 使用
os.dup2()配合线程读取管道:仅能抑制输出,无法获取具体内容。 - 调用
kernel32.GetStdHandle()和SetStdHandle():修改句柄后打印会触发错误OSError: [WinError 6] 句柄无效。 - 依赖
ctypes.util.find_msvcrt()的方案:Python 3.5+中该函数返回None(微软已从MSVCRT转向UCRT),即使改为直接调用ctypes.cdll.msvcrt,也仅能抑制输出,无法捕获内容。
有效解决方案
基于Windows管道API的重定向方案
通过创建匿名管道替换标准句柄,同时处理UCRT的句柄缓存问题,确保C库输出能被正确捕获。
代码示例:
import ctypes from ctypes import wintypes # 加载Windows核心库和UCRT库 kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) ucrtbase = ctypes.WinDLL('ucrtbase', use_last_error=True) # 定义Windows API常量 STD_ERROR_HANDLE = -12 GENERIC_READ = 0x80000000 FILE_SHARE_READ = 0x00000001 INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value # 定义安全属性结构体 class SECURITY_ATTRIBUTES(ctypes.Structure): _fields_ = [ ('nLength', wintypes.DWORD), ('lpSecurityDescriptor', wintypes.LPVOID), ('bInheritHandle', wintypes.BOOL) ] # 绑定Windows API函数类型 kernel32.CreatePipe.restype = wintypes.BOOL kernel32.CreatePipe.argtypes = [ wintypes.PHANDLE, wintypes.PHANDLE, ctypes.POINTER(SECURITY_ATTRIBUTES), wintypes.DWORD ] kernel32.ReadFile.restype = wintypes.BOOL kernel32.ReadFile.argtypes = [ wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD, wintypes.LPDWORD, wintypes.LPVOID ] kernel32.CloseHandle.restype = wintypes.BOOL kernel32.CloseHandle.argtypes = [wintypes.HANDLE] kernel32.GetStdHandle.restype = wintypes.HANDLE kernel32.GetStdHandle.argtypes = [wintypes.DWORD] kernel32.SetStdHandle.restype = wintypes.BOOL kernel32.SetStdHandle.argtypes = [wintypes.DWORD, wintypes.HANDLE] # 绑定UCRT的句柄缓存操作函数 ucrtbase.__acrt_iob_func.restype = ctypes.POINTER(ctypes.c_char * 2048) ucrtbase.__acrt_iob_func.argtypes = [wintypes.INT] def capture_dll_stderr(): # 创建匿名管道 sa = SECURITY_ATTRIBUTES() sa.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) sa.bInheritHandle = True sa.lpSecurityDescriptor = None read_handle, write_handle = wintypes.HANDLE(), wintypes.HANDLE() if not kernel32.CreatePipe(ctypes.byref(read_handle), ctypes.byref(write_handle), ctypes.byref(sa), 0): raise ctypes.WinError(ctypes.get_last_error()) # 保存原stderr句柄 original_stderr = kernel32.GetStdHandle(STD_ERROR_HANDLE) try: # 设置新的stderr句柄 if not kernel32.SetStdHandle(STD_ERROR_HANDLE, write_handle): raise ctypes.WinError(ctypes.get_last_error()) # 更新UCRT的stderr句柄缓存,避免C库使用旧句柄 stderr_iob = ucrtbase.__acrt_iob_func(2) # 2对应stderr ctypes.cast(stderr_iob[0], ctypes.POINTER(wintypes.HANDLE))[0] = write_handle # 执行产生输出的DLL调用 string = b'hello\n' ctypes.cdll.msvcrt._write(2, string, len(string)) # 强制刷新C库输出缓冲区 ucrtbase.fflush(stderr_iob) # 读取管道中的内容 buffer = ctypes.create_string_buffer(4096) bytes_read = wintypes.DWORD() kernel32.ReadFile(read_handle, buffer, ctypes.sizeof(buffer), ctypes.byref(bytes_read), None) return buffer.value[:bytes_read.value].decode('utf-8') finally: # 恢复原stderr句柄 kernel32.SetStdHandle(STD_ERROR_HANDLE, original_stderr) stderr_iob = ucrtbase.__acrt_iob_func(2) ctypes.cast(stderr_iob[0], ctypes.POINTER(wintypes.HANDLE))[0] = original_stderr # 关闭管道句柄 kernel32.CloseHandle(read_handle) kernel32.CloseHandle(write_handle) # 测试捕获结果 captured = capture_dll_stderr() print(f"捕获的stderr内容:{repr(captured)}")
方案说明
- 利用
CreatePipe创建匿名管道,将stderr重定向到管道写入端 - 通过
__acrt_iob_func获取UCRT的标准流结构体,更新其中的句柄,避免C库缓存旧句柄导致输出丢失 - 调用
fflush强制刷新C库缓冲区,确保内容写入管道 - 读取管道内容后恢复原句柄并清理资源
注意事项
- 该方案兼容UCRT(Python3.5+默认使用)和MSVCRT
- 如果DLL使用mingw等其他C运行时,需将
__acrt_iob_func替换为对应运行时的句柄函数(如mingw的__iob_func)
内容的提问来源于stack exchange,提问作者1''
相关产品推荐
相关产品推荐

