【问题标题】:Reading Unicode from redirected STDOUT (C++, Win32 API, Qt)从重定向的 STDOUT(C++、Win32 API、Qt)读取 Unicode
【发布时间】:2011-03-12 17:57:40
【问题描述】:

我有一个动态加载插件 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 之后,我决定放弃这个解决方案并恢复到 cout/wcout 缓冲区替换,并要求 DLL 调用初始化方法,原因有两个:UTF16 没有传递到我的缓冲区,然后是找出编码的问题在缓冲区中。但是,我仍然很好奇是否有一种方法可以通过 C 运行时将 UTF-16 字符串“按原样”放入管道中,而不需要依赖于语言环境的转换?

附言也欢迎任何关于 cout/wcout 重定向到 UI 而不是提到的两种方法的建议:)

提前谢谢你!

【问题讨论】:

  • 你说得对,问题在于运行时如何处理(根据标准)std::wcout。如果发送者和接收者都使用普通的 Windows API 函数,这将是最简单的方法。
  • 谢谢菲利普!由于遗留问题,插件使用 cout/wcout,我必须让它们工作。
  • 如果他们写信给cout,您可能会尝试强制他们使用UTF-8,但我不知道如何处理wcout,因为标准需要修改某些本地编码通常是过时的 8 位编码。
  • Philipp,您能告诉我您实际指的是什么标准吗?

标签: c++ winapi qt runtime


【解决方案1】:

这里的问题是,从 wchar_tchar 的代码转换完全在插件 DLL 内完成,无论它碰巧使用的是 cout/wcout 实现(正如你所说可能与主应用程序正在使用的不同)。因此,让它表现不同的唯一方法是以某种方式拦截该机制,例如使用streambuf 替换。

但是,正如您所暗示的,您在主应用程序中编写的任何代码都不一定与 DLL 使用的库实现兼容。例如,如果您在主应用程序中实现流缓冲区,它不一定使用与 DLL 中的流缓冲区相同的 ABI。所以这是有风险的。

我建议你实现一个封装DLL,它使用与插件相同的C++库版本,这样可以保证兼容,并且在这个封装DLL中对cout/wcout进行必要的干预。它可以动态加载插件,因此可以与使用该库版本的任何插件重用。或者,您可以创建一些可重用的源代码,专门为每个插件编译,从而生成每个插件的净化版本。

打包 DLL 后,您可以将流缓冲区替换为 cout/wcout,将数据保存到内存中,正如我认为您最初计划的那样,并且根本不必弄乱文件句柄。

PS:如果您确实需要制作一个与 UTF-8 相互转换的 wstream,那么我建议您使用 Boost 的 utf8_codecvt_facet 作为一种非常简洁的方法。它易于使用,文档中有示例代码。 (在这种情况下,您必须专门为插件正在使用的库版本编译一个 Boost 版本,但不是在一般情况下。)

【讨论】:

    【解决方案2】:

    试试:

    std::wcout.imbue(std::locale("en_US.UTF-8"));
    

    这是特定于流的,比使用全局 C 库 setlocale() 更好。

    但是,您可能需要调整语言环境名称以适应您的运行时支持的内容。

    【讨论】:

    • 这里的问题至少是语言环境是未知的。字符串是 UCS2 编码的,但无法事先知道语言环境。这就是为什么整个区域设置方法是不可接受的。不过感谢提示,我将检查将语言环境设置为 std::wcout 如何影响输出。
    • @Sergei Eliseev:实际上,它根本不依赖于用户的语言环境,因为语言环境是按名称显式构建的,并且仅与这一流相关联。其他流可以有其他语言环境。恕我直言,摆脱全局语言环境是 C++ 标准库做对的一件好事。
    • @Sergei Eliseev:我终于借了一台 Windows 机器试一试,我发现 Windows(至少是 XP)不支持在语言环境名称中使用 UTF-8 代码页。 IOW,std::locale("English_USA.UTF-8") 抛出 std::runtime_error("bad locale name") 而 std::locale("English_USA.1252") 是可以的。我在网上查了一下,我认为这是因为 UTF-8 可以为每个 Unicode 字符生成两个以上的字节。因此,不幸的是,我的解决方案在 Windows 上不起作用,即使它在 Linux 上也是如此。对于那个很抱歉。以后我会在 Windows 上检查一下,而不是做任何假设。
    【解决方案3】:

    我不知道这是否可能,但也许您可以在单独的进程中启动 DLL,并使用 Windows 等效的 pipe 捕获该进程的输出(无论如何,但 Qt 的 QProcess 应该为你处理好)。这将类似于 Firefox 处理进程外插件的方式(默认在 3.6.6 中,但在 64 位 Firefox 和 32 位 Flash 插件中已经完成了一段时间)。您必须想出一些方法来在单独的进程中与 DLL 进行通信,例如共享内存,但这应该是可能的。不一定漂亮,但可能。

    【讨论】:

    • 这很有趣!我不确定它在我的特定情况下是否实用,但总体思路很有趣。出于好奇,我一定会尝试的。
    • @Sergei:我想你仍然会遇到插件的 wcout 在尝试输出非 ascii 字符时会阻塞的问题。
    猜你喜欢
    • 2012-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-06
    相关资源
    最近更新 更多