Windows命令行窗口与Unicode

一.

还记得刚开始学习C语言的时候,最早接触的runtime函数,就是printf,这应该是一个最基础的函数了。
但是这样的一个函数,即使在VC2005和Windows 7下面,对输出Unicode字符这样的任务,依然会给程序员带了麻烦。

下面的代码是一个Unicode版本的测试程序:

int wmain(int argc, wchar_t **argv)
{
    wprintf(L"测试");

    return 0;
}
看着完全没有问题,但是在console中就是没有任何的输出。 二. 单步进入wprintf,发现如下代码:
        retval = _woutput_l(stdout,format,NULL,arglist);
其中,宏stdout为标准输出,定义如下:
#define stdout (&__iob_func()[1])
__iob_func的代码如下:
/* These functions are for enabling STATIC_CPPLIB functionality */
_CRTIMP FILE * __cdecl __iob_func(void)
{
    return _iob;
}
_iob是一个FILE类型的数组,第0个元素为stdin,第1个元素为stdout,第3个为stderr。 而FILE类型是一个结构体:
struct _iobuf {
        char *_ptr;
        int   _cnt;
        char *_base;
        int   _flag;
        int   _file;
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
        };
typedef struct _iobuf FILE;
_iob在调试器中的信息 (点击查看原图): [![information of _iob](http://blog.deskangel.com/wp-content/uploads/2010/02/sc_v22281_13.png "_iob在调试器中的信息")](http://blog.deskangel.com/wp-content/uploads/2010/02/sc_v22281_13.png) 从图中可以看出,std本身就不支持UNICODE。 三. 我们还可以更深入一点,进入_woutput_l函数,会发现该函数调用 WRITE_CHAR(ch, &charsout); 宏逐个输出字符。 该宏的定义和_woutput_l在同一个文件(output.c)中找到:
#define WRITE_CHAR(ch, pnw)         write_char(ch, stream, pnw)
其中ch为第一个字符,“测”;stream为_woutput_l的第一个参数,就是stdout。 四. write_char的代码很简单:
LOCAL(void) write_char (
    _TCHAR ch,
    FILE *f,
    int *pnumwritten
    )
{
    if ( (f->_flag & _IOSTRG) && f->_base == NULL)
    {
        ++(*pnumwritten);
        return;
    }
#ifdef _UNICODE
    if (_putwc_nolock(ch, f) == WEOF)
#else  /* _UNICODE */
    if (_putc_nolock(ch, f) == EOF)
#endif  /* _UNICODE */
        *pnumwritten = -1;
    else
        ++(*pnumwritten);
}
五. 进入_fputwc_nolock(即_putwc_nolock)。该函数的声明为:
wint_t __cdecl _fputwc_nolock (
        wchar_t ch,
        FILE *str
        )
函数中对str有三个判断,
if (_textmode_safe(_fileno(str)) == __IOINFO_TM_UTF16LE)
{
...
}
else if (_textmode_safe(_fileno(str)) == __IOINFO_TM_UTF8)
{
...
}
else if ((_osfile_safe(_fileno(str)) & FTEXT))
{
...
}
第一第二个判断很明显,判断目标文件是否为UTF16 little endian编码和UTF8编码。 对stdout,这里会进入第三个判断,其中FTEXT表示文件句柄为文本模式。 在该条件下,函数接着调用wctomb_s:
wctomb_s(&size, mbc, MB_LEN_MAX, ch)
做编码转换,即试图把wide char的ch转换为multi-byte的mbc。 六. wctomb_s调用_wctomb_s_l,仅仅把locale参数设为NULL:
extern "C" errno_t __cdecl wctomb_s (
        int *pRetValue,
        char *dst,
        size_t sizeInBytes,
        wchar_t wchar
        )
{
        return _wctomb_s_l(pRetValue, dst, sizeInBytes, wchar, NULL);
}
七. _wctomb_s_l函数是wctomb_s的locale版,根据locale做不同的工作。声明如下:
extern "C" int __cdecl _wctomb_s_l (
        int *pRetValue,
        char *dst,
        size_t sizeInBytes,
        wchar_t wchar,
        _locale_t plocinfo
        )
函数对locale有一个判断,
if ( _loc_update.GetLocaleT()->locinfo->lc_handle[LC_CTYPE] == _CLOCALEHANDLE )
{
        if ( wchar > 255 )  /* validate high byte */
        {
            if (dst != NULL && sizeInBytes > 0)
            {
                memset(dst, 0, sizeInBytes);
            }
            errno = EILSEQ;
            return errno;
        }

        if (dst != NULL)
        {
            _VALIDATE_RETURN_ERRCODE(sizeInBytes > 0, ERANGE);
            *dst = (char) wchar;
        }
        if (pRetValue != NULL)
        {
            *pRetValue = 1;
        }
        return 0;
}
else
{
...
}
如果locale是C locale的话,那么当wchar大于255,即超出ANSI范围时,不做转换,直接返回; 小于255,即为ANSI字符时,则转换为char类型。 如果不是c locale,则调用WideCharToMultiByte
        if ( ((size = WideCharToMultiByte( _loc_update.GetLocaleT()->locinfo->lc_codepage,
                                           0,
                                           &wchar,
                                           1,
                                           dst,
                                           (int)sizeInBytes,
                                           NULL,
                                           &defused) ) == 0) ||
             (defused) )
测试代码没有设置locale,所以判断wchar,自然大于255,函数直接返回。层层返回,没有调用任何实际的输出。 八. 稍稍修改一下测试程序:
int wmain(int argc, wchar_t **argv)
{
    wprintf(L"%s", L"测试");

    return 0;
}
输出的是两个问号。单步跟踪,我们可以发现,在_woutput_l中,没有直接调用WRITE_CHAR,而是调用WRITE_STRING
WRITE_STRING(text.wz, textlen, &charsout);

WRITE_STRING的代码:

LOCAL(void) write_string (
    _TCHAR *string,
    int len,
    FILE *f,
    int *pnumwritten
    )
{
    if ( (f->_flag & _IOSTRG) && f->_base == NULL)
    {
        (*pnumwritten) += len;
        return;
    }
    while (len-- > 0) {
        write_char(*string++, f, pnumwritten);
        if (*pnumwritten == -1)
        {
            if (errno == EILSEQ)
                write_char(_T('?'), f, pnumwritten);
            else
                break;
        }
    }
}
write_char写入Unicode字符失败后,就会写入一个"?",所以输出的会是问号。 九. 如何输出Unicode呢?有两个方法: 1\. 设置正确的locale。 我们看到整个过程最后会调用WideCharToMultiByte把Unicode根据locale转换成multiple byte。 这可以解决一点问题,但不完美。除了绕了个圈,又把wide char转换成multiple byte之外,还无法解决多种语言混合输出的问题。 比如,无法同时输出俄文和中文。 2\. 使用WriteConsole。 如下代码可以完美的解决Unicode的问题:
    wchar_t szText[] = L"测试";

    HANDLE hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    WriteConsole(hStdOutput, szText, (DWORD)wcslen(szText), NULL, NULL);