一.

还记得刚开始学习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

从图中可以看出,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);