如何将stderr和std::cerr重定向到stringstream并恢复stderr?
捕获C库stderr输出的问题与解决方案
问题背景
C库libmodbus通过fprintf(stderr, "")输出调试信息,在C++应用中需要捕获这些输出,但现有方案仅能捕获std::cerr的内容,无法处理C层面的stderr输出。尝试用setbuf等方法时,因stderr是写-only流导致读取失败,实现重定向类后,遇到了freopen重定向后无法恢复到控制台的问题,同时提出三个核心问题:
1. 调用freopen("nul", "a", stderr)后,如何将stderr恢复到控制台?
要恢复stderr到控制台,关键是提前保存原始stderr的文件描述符——freopen会替换FILE*关联的文件对象,但原始控制台的文件描述符并未被销毁。具体实现步骤:
- 在类的构造函数中,先保存原始stderr的文件描述符(需在类中新增
int _orig_stderr_fd;成员变量):#ifdef _WIN32 _orig_stderr_fd = _dup(_fileno(c_handle)); // Windows下用_dup和_fileno #else _orig_stderr_fd = dup(fileno(c_handle)); // 类Unix下用dup和fileno #endif - 在析构函数中,将保存的文件描述符恢复到stderr,并重新关联控制台设备:
~fstream_redirect() { // 恢复C++流 _fstream->rdbuf(_old); // 恢复C的stderr if (_orig_stderr_fd != -1) { #ifdef _WIN32 _dup2(_orig_stderr_fd, _fileno(_file)); freopen("CON", "a", _file); // Windows控制台设备路径为CON _close(_orig_stderr_fd); #else dup2(_orig_stderr_fd, fileno(_file)); freopen("/dev/stderr", "a", _file); // 类Unix下关联回stderr设备 close(_orig_stderr_fd); #endif } }
2. 是否有更简便的实现方式?
有三种更高效的替代方案:
- 利用libmodbus内置接口:libmodbus提供
modbus_set_debug函数,部分版本支持自定义回调直接获取调试信息,无需捕获stderr,这是最直接的方式,避免了流重定向的复杂度。 - 匿名管道重定向:创建匿名管道,将stderr重定向到管道写端,启动线程持续读取管道读端内容并写入
stringstream。这种方式无需手动管理缓冲区,能实时捕获输出:- 创建管道(Linux用
pipe(),Windows用CreatePipe()) - 保存原始stderr文件描述符,用
dup2()将stderr重定向到管道写端 - 启动线程循环读取管道读端数据,写入目标缓冲区
- 恢复时将原始文件描述符放回stderr,关闭管道
- 创建管道(Linux用
- 自定义streambuf绑定:实现自定义
std::streambuf,将写入内容同时转发到原始stderr和stringstream,替换std::cerr的rdbuf,再用freopen将C的stderr关联到该streambuf对应的文件描述符。
3. 当前方案的可移植性如何?
当前方案的可移植性存在明显局限:
- 平台依赖细节:空设备路径(
nul//dev/null)仅覆盖Windows和类Unix,小众平台可能不兼容;文件描述符操作函数(dup/_dup、fileno/_fileno)也存在平台差异。 - 缓冲区行为不确定性:
setvbuf指定的_IOFBF(全缓冲)行为依赖系统实现,部分平台可能不会将fprintf内容写入指定的全局缓冲区,rewind对/dev/null的操作也可能不符合预期。 - 线程安全问题:全局缓冲区
fstream_redirect_buffer未做同步,多线程写入stderr时会出现数据混乱。 - 非实时捕获:必须主动调用
update()方法才能读取缓冲区内容,容易导致输出延迟或丢失。
修改后的完整代码示例
fstream_redirect.h
#ifndef FSTREAM_REDIRECT_H #define FSTREAM_REDIRECT_H #include <iostream> #include <sstream> #include <cstdio> #ifdef _WIN32 #include <io.h> #endif char fstream_redirect_buffer[BUFSIZ+1]; class fstream_redirect { public: fstream_redirect(std::stringstream &new_buffer, std::ostream &fstream, FILE* c_handle) : _orig_stderr_fd(-1) { // 保存C++流相关指针 _new_buffer = &new_buffer; _old = fstream.rdbuf(); _fstream = &fstream; // 重定向C++流到stringstream fstream.rdbuf(new_buffer.rdbuf()); // 保存原始stderr文件描述符 #ifdef _WIN32 _orig_stderr_fd = _dup(_fileno(c_handle)); #else _orig_stderr_fd = dup(fileno(c_handle)); #endif // 重定向C的stderr到空设备 #ifdef _WIN32 _file = freopen("nul", "a", c_handle); #else _file = freopen("/dev/null", "a", c_handle); #endif // 设置自定义缓冲区 if (setvbuf(_file, fstream_redirect_buffer, _IOFBF, BUFSIZ)) { std::cout << "setvbuf failed" << std::endl; } } ~fstream_redirect() { // 恢复C++流 _fstream->rdbuf(_old); // 恢复C的stderr到控制台 if (_orig_stderr_fd != -1) { #ifdef _WIN32 _dup2(_orig_stderr_fd, _fileno(_file)); freopen("CON", "a", _file); _close(_orig_stderr_fd); #else dup2(_orig_stderr_fd, fileno(_file)); freopen("/dev/stderr", "a", _file); close(_orig_stderr_fd); #endif } } void update() { fpos_t pos; char *p = fstream_redirect_buffer; fgetpos(_file, &pos); if (pos) { p[pos] = 0; *_new_buffer << p; rewind(_file); } } private: fstream_redirect() = default; std::stringstream* _new_buffer; std::streambuf* _old; std::ios* _fstream; FILE* _file; int _orig_stderr_fd; }; #endif
main.cpp
#include "fstream_redirect.h" #include <iostream> std::stringstream _gError_buffer; fstream_redirect _gError_redirect(_gError_buffer, std::cerr, stderr); int main() { fprintf(stderr, "1"); std::cerr << "2"; fprintf(stderr, "3"); std::cerr << "4"; fprintf(stderr, "5"); std::cerr << "6"; std::cout << _gError_buffer.str() << std::endl; // 输出:246 _gError_redirect.update(); std::cout << _gError_buffer.str() << std::endl; // 输出:246135 // 测试恢复后的stderr输出 fprintf(stderr, "恢复后的stderr输出\n"); std::cerr << "恢复后的cerr输出\n"; return 0; }
内容的提问来源于stack exchange,提问作者Nor
相关产品推荐
相关产品推荐

