我有一个动态加载插件DLL的C ++应用程序。 DLL通过std :: cout和std :: wcout发送文本输出。基于Qt的UI必须从DLL中获取所有文本输出并显示它。 由于运行时库差异,DLL可能具有不同的cout / wcout实例,因此使用流缓冲区替换的方法并不完全有效。因此,我按如下方式应用了Windows特定的STDOUT重定向:
StreamReader::StreamReader(QObject *parent) :
QThread(parent)
{
// void
}
void StreamReader::cleanUp()
{
// restore stdout
SetStdHandle (STD_OUTPUT_HANDLE, oldStdoutHandle);
CloseHandle(stdoutRead);
CloseHandle(stdoutWrite);
CloseHandle (oldStdoutHandle);
hConHandle = -1;
initDone = false;
}
bool StreamReader::setUp()
{
if (initDone)
{
if (this->isRunning())
return true;
else
cleanUp();
}
do
{
// save stdout
oldStdoutHandle = ::GetStdHandle (STD_OUTPUT_HANDLE);
if (INVALID_HANDLE_VALUE == oldStdoutHandle)
break;
if (0 == ::CreatePipe(&stdoutRead, &stdoutWrite, NULL, 0))
break;
// redirect stdout, stdout now writes into the pipe
if (0 == ::SetStdHandle(STD_OUTPUT_HANDLE, stdoutWrite))
break;
// new stdout handle
HANDLE lStdHandle = ::GetStdHandle(STD_OUTPUT_HANDLE);
if (INVALID_HANDLE_VALUE == lStdHandle)
break;
hConHandle = ::_open_osfhandle((intptr_t)lStdHandle, _O_TEXT);
FILE *fp = ::_fdopen(hConHandle, "w");
if (!fp)
break;
// replace stdout with pipe file handle
*stdout = *fp;
// unbuffered stdout
::setvbuf(stdout, NULL, _IONBF, 0);
hConHandle = ::_open_osfhandle((intptr_t)stdoutRead, _O_TEXT);
if (-1 == hConHandle)
break;
return initDone = true;
} while(false);
cleanUp();
return false;
}
void StreamReader::run()
{
if (!initDone)
{
qCritical("Stream reader is not initialized!");
return;
}
qDebug() << "Stream reader thread is running...";
QString s;
DWORD nofRead = 0;
DWORD nofAvail = 0;
char buf[BUFFER_SIZE+2] = {0};
for(;;)
{
PeekNamedPipe(stdoutRead, buf, BUFFER_SIZE, &nofRead, &nofAvail, NULL);
if (nofRead)
{
if (nofAvail >= BUFFER_SIZE)
{
while (nofRead >= BUFFER_SIZE)
{
memset(buf, 0, BUFFER_SIZE);
if (ReadFile(stdoutRead, buf, BUFFER_SIZE, &nofRead, NULL)
&& nofRead)
{
s.append(buf);
}
}
}
else
{
memset(buf, 0, BUFFER_SIZE);
if (ReadFile(stdoutRead, buf, BUFFER_SIZE, &nofRead, NULL)
&& nofRead)
{
s.append(buf);
}
}
// Since textReady must emit only complete lines,
// watch for LFs
if (s.endsWith('\n')) // may be emmitted
{
emit textReady(s.left(s.size()-2));
s.clear();
}
else // last line is incomplete, hold emitting
{
if (-1 != s.lastIndexOf('\n'))
{
emit textReady(s.left(s.lastIndexOf('\n')-1));
s = s.mid(s.lastIndexOf('\n')+1);
}
}
memset(buf, 0, BUFFER_SIZE);
}
}
// clean up on thread finish
cleanUp();
}
但是,这个解决方案似乎有一个障碍 - C运行时库,它依赖于语言环境。因此,发送到wcout的任何输出都没有到达我的缓冲区,因为C运行时会截断UTF-16编码字符串中存在的非可打印ASCII字符的字符串。调用 setlocale()演示,C运行时执行字符串重新/编码。 setlocale()对我来说没有任何帮助,因为不了解文本的语言或语言环境,因为插件DLL从系统外部读取并且可能混合了不同的语言。 在给出一个N-thought之后我决定放弃这个解决方案并恢复到cout / wcout缓冲区替换并将DLL的要求调用初始化方法由于两个原因:UTF16没有传递给我的缓冲区,然后是计算编码的问题在缓冲区。但是,我仍然很想知道是否有办法通过C运行时将UTF-16字符串“按原样”输入管道,而不进行与语言环境相关的转换?
P.S。关于cout / wcout重定向到UI的任何建议,而不是两种提到的方法也是受欢迎的:)
提前谢谢!
答案 0 :(得分:1)
这里的问题是从wchar_t
到char
的代码转换完全在插件DLL中完成,无论发生cout
/ wcout
实现使用(正如你所说的那样可能与主应用程序使用的那个不一样)。因此,使其行为不同的唯一方法是以某种方式拦截该机制,例如替换streambuf
。
但是,正如您所暗示的那样,您在主应用程序中编写的任何代码都不一定与DLL使用的库实现兼容。例如,如果在主应用程序中实现流缓冲区,则它不一定会使用与DLL中的流缓冲区相同的ABI。所以这很危险。
我建议你实现一个使用与插件相同的C ++库版本的包装器DLL,因此它保证兼容,并且在这个包装器DLL中对cout
/ {{1}进行必要的干预}。它可以动态加载插件,因此可以使用任何使用该库版本的插件重用。或者,您可以创建一些可重用的源代码,可以为每个插件专门编译,从而生成每个插件的清理版本。
一旦DLL被包装,您可以将流缓冲区替换为wcout
/ cout
,将数据保存到内存中,就像我认为您最初计划的那样,而不必弄乱文件句柄一点都不。
utf8_codecvt_facet
作为一种非常巧妙的方式。它易于使用,文档中包含示例代码。
(在这种情况下,您必须专门为插件使用的库版本编译Boost版本,但在一般情况下不会。)
答案 1 :(得分:0)
我不知道这是否可行,但也许你可以在一个单独的进程中启动DLL并使用Windows等效的pipe
捕获该进程的输出(不管是什么,但是Qt的{{ 1}}应该照顾你)。这与Firefox的进程外插件类似(默认情况下为3.6.6,但已经使用64位Firefox和32位Flash插件完成了一段时间)。你必须想出一些在单独进程中与DLL通信的方法,比如共享内存,但它应该是可能的。不一定漂亮,但可能。
答案 2 :(得分:0)
尝试:
std::wcout.imbue(std::locale("en_US.UTF-8"));
这是特定于流的,并且比使用全局C库setlocale()
更好。
但是,您可能必须调整区域设置名称以适应运行时支持的内容。