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

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