[C++] MSVC 왜 wprintf는 되고 printf는 되지 않는 것일까?

아래와 같은 간단한 코드를 살펴보자. 1252 코드페이지로 '안녕’을 변환해서 출력하는 예제다. 출력을 해보면 printf는 begin까지만 출력되고 wprintf는 end까지 모두 출력된다. 이를두고 printf는 안시 함수라 유니코드 문자열을 정상적으로 출력하지 못하고, wprintf는 유니코드 함수라 정상적으로 출력한다면 오산이다. 왜냐하면 둘다 출력은 틀렸기 때문이다. wprintf가 출력한게 한글 "안녕"이 아니라는 의미다. 한글은 우리가 잘 아는 문자라 이런 오류를 쉽게 인지할 수 있지만 모르는 문자 속에 섞여 있으면 자칫 wprintf는 제대로 출력한다는 오해를 할수도 있다. 그렇다면 왜 printf는 실패하고, wprintf는 끝까지 출력할까?

int main()
{
	// windows-1252	ANSI Latin 1; Western European (Windows)
	// 한글 변환이 불가능한 코드 페이지

	setlocale(LC_ALL, ".1252");

	wchar_t *msg = L"안녕";

	printf("printf begin %ls end", msg);
	printf("\n");
	wprintf(L"wprintf begin %ls end", msg);
	printf("\n");

	return 0;
}

이유는 간단하다. 그렇게 만들었기 때문이다. 먼저 printf부터 살펴보자. 결국 아래 코드로 들어간다. _WCTOMB_S가 변환에 실패하면 루프를 제어하는 변수 charsout이 -1로 설정되고 루프가 종료되어 더 이상 출력이 진행되지 않는다.

_output_l(...)
{
	// ...

	while ((ch = *format++) != _T('\0') && charsout >= 0)
	{
                    while (count--) {
                        e = _WCTOMB_S(&retval, L_buffer, _countof(L_buffer), *p++);
                        if (e != 0 || retval == 0) {
                            charsout = -1;
                            break;
                        }
                        WRITE_STRING(L_buffer, retval, &charsout);
                    }

	}

	// ...

}

반면 wprintf는 아래와 같이 전개되고, 변환에 실패하면 ?를 출력하고 성공인 척 하도록 되어 있다. 즉, wprintf라고 정상 출력을 해내는 것이 아니다.

_output_l()
{
	// ...

	while ((ch = *format++) != _T('\0') && charsout >= 0)
	{
		WRITE_STRING(text.wz, textlen, &charsout);
	}

	// ...

}

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 함수를 따라가보면 결국 _fputwc_noblock으로 이어지고 콘솔인 stdout은 텍스트 모드라 printf와 동일하게 wctomb_s로 변환을 시도한다. 다만 여기서 실패해도 위쪽에서 살펴본 if문에 의해서 ?를 출력하고 계속 출력을 이어가도록 만들어둔 것이다.

wint_t __cdecl _fputwc_nolock (
        wchar_t ch,
        FILE *str
        )
{
	// ...


        if (!(str->_flag & _IOSTRG))
        {
            if (_textmode_safe(_fileno(str)) == __IOINFO_TM_UTF16LE
                    || _textmode_safe(_fileno(str)) == __IOINFO_TM_UTF8)
            {
                /* binary (Unicode) mode */
                if ( (str->_cnt -= sizeof(wchar_t)) >= 0 ) {
                    return (wint_t) (0xffff & (*((wchar_t *)(str->_ptr))++ = (wchar_t)ch));
                } else {
                    return (wint_t) _flswbuf(ch, str);
                }
            }
            else if ((_osfile_safe(_fileno(str)) & FTEXT))
			{
				// stdout은 이쪽으로 빠진다.
				// 결국 wctomb_s 함수로 printf와 동일하게 변환을 시도한다.

                int size, i;
                char mbc[MB_LEN_MAX];

                /* text (multi-byte) mode */
                if (wctomb_s(&size, mbc, MB_LEN_MAX, ch) != 0)
                {
                        /*
                         * Conversion failed; errno is set by wctomb_s;
                         * we return WEOF to indicate failure.
                         */
                        return WEOF;
                }

	// ...
}

간혹 깨진 문자열을 보고 UTF-16으로 바로 출력됐다는 소리를 하는 황당한 경우도 있다. UTF-16으로 바로 출력하고 싶다면 printf와 같은 텍스트 계열 함수가 아닌 fwrite 같은 바이너리 계통의 함수를 사용해야 한다. 아래와 같이 출력하면 UTF-16으로 출력된다.

fwrite(msg, sizeof(*msg), wcslen(msg), stdout);

(사족) 가끔 느끼는 거지만 자기가 지금 짜고 있는 코드가 정확하게 무슨 일을 하고 있는지 알고 있는 프로그래머가 생각보다 드문 것 같다. 토이 프로그램만 만들게 아니라면 자기가 짠 코드나 라이브러리가 정확하게 무슨 일을 하는지 알고 있는 것은 무척 중요하다. 컴퓨터는 복잡하고 코드는 추상적이기 때문에 프로그래머는 공부를 열심히 해야 한다. 내가 생각하는 프로그래머라는 업의 본질은 그런 것 같다.