지난 글에서 우리는 setlocale에서 UTF-7이나 UTF-8을 지정할 경우 실패한다는 것을 알게 되었다. 하지만 여전히 의문은 남는다. 왜 MS는 setlocale에서 흔해빠진 UTF-8 따위를 지원하지 않을까? 도대체 무엇이 우리를 가로 막는 것일까? 안 될 이유가 없는게 안되면 한번쯤은 내부가 궁금해지게 마련이다. 그래서 이런 간단한 코드를 디버깅 해보기로 했다.
int main()
{
printf("%p\n", setlocale(LC_CTYPE, "Korean_Korea.65001"));
return 0;
}
디버깅 결과 총 세 지점에 우리를 가로막는 허들이 존재한다는 사실을 알게 되었다. 첫번째는 아래 지점이다. 대놓고 CP_UTF7과 CP_UTF8은 안 된다는 소리다. 재미난 사실은 if 문에 이 코드가 들어간 것이 Visual Studio의 특정 버전 부터라는 점이다. Visual C++ 6.0에는 CP_UTF7과 CP_UTF8을 검사하는 코드가 없다. 단지 IsValidCodePage로만 체크한다.
BOOL __cdecl __get_qualified_locale(const LPLC_STRINGS lpInStr, LPLC_ID lpOutId,
LPLC_STRINGS lpOutStr)
{
// ...
// verify codepage validity
if (!iCodePage || iCodePage == CP_UTF7 || iCodePage == CP_UTF8 ||
!IsValidCodePage((WORD)iCodePage))
return FALSE;
// ...
}
다음 허들은 아래 지점이다. 현재 코드페이지로 변환 되는 최대 글자 개수가 MB_LEN_MAX보다 크면 안 된다는 소리다. MB_LEN_MAX의 경우 Visual Studio 2015는 5로 정의되어 있다. 따라서 이 코드는 넘어간다. 반면에 Visual C++ 6.0에서는 MB_LEN_MAX가 2고 CP_UTF8은 이 구문을 넘지 못하고 실패한다.
int __cdecl __init_ctype (
pthreadlocinfo ploci
)
{
// ...
if (lpCPInfo.MaxCharSize > MB_LEN_MAX)
goto error_cleanup;
// ...
}
마지막 허들은 아래 지점이다. MultiByteToWideChar 함수를 통해서 버퍼가 얼마나 커야 하는지 구하는 부분이다. code_page에 CP_UTF8이 들어 있으면 플래그로 0이나 MB_ERR_INVALID_CHARS가 전달되어야 하지만 여기에서는 MB_PRECOMPOSED가 합쳐서 전달된다. MSDN 페이지에 나와 있는 것처럼 ERROR_INVALID_FLAGS로 실패한다.
static BOOL __cdecl __crtGetStringTypeA_stat(
_locale_t plocinfo,
DWORD dwInfoType,
LPCSTR lpSrcStr,
int cchSrc,
LPWORD lpCharType,
int code_page,
int lcid,
BOOL bError
)
{
// ...
/* find out how big a buffer we need */
if ( 0 == (buff_size = MultiByteToWideChar( code_page,
bError ?
MB_PRECOMPOSED |
MB_ERR_INVALID_CHARS
: MB_PRECOMPOSED,
lpSrcStr,
cchSrc,
NULL,
0 )) )
return FALSE;
// ...
}
여기까지 따라오면 대충 뭔가 감이 잡힌다. 마지막 MSDN의 주석에 있던 코드페이지를 넘기면 setlocale은 죄다 실패한다. 그리고 그 코드 페이지가 가지고 있는 대체적인 특징은 한 글자가 2바이트가 넘는 3바이트, 4바이트 등으로 변환되는 코드페이지라는 점이다.
For the code pages listed below, dwFlags must be set to 0. Otherwise, the function fails with ERROR_INVALID_FLAGS.
50220
50221
50222
50225
50227
50229
57002 through 57011
65000 (UTF-7)
42 (Symbol)
Note For UTF-8 or code page 54936 (GB18030, starting with Windows Vista), dwFlags must be set to either 0 or MB_ERR_INVALID_CHARS. Otherwise, the function fails with ERROR_INVALID_FLAGS.
— MultiBytesToWideChar MSDN
다시 setlocale의 MSDN 페이지를 살펴보자. UTF-7과 UTF-8을 넣으면 실패한다는 앞 문장에 의미심장한 설명이 있다. 한 글자당 두 바이트를 넘게 사용하는 코드 페이지는 예외라는 설명이다. 예외~ 즉, UTF-8이 안 되는 것이 아니라 한 글자에 두 바이트를 넘게 쓰는 코드페이지는 죄다 setlocale로 지정할 수 없고, wcstombs나 mbstowcs 함수로 변환할 수 없다는 것을 의미한다.
The set of available locale names, languages, country/region codes, and code pages includes all those supported by the Windows NLS API except code pages that require more than two bytes per character, such as UTF-7 and UTF-8.
— setlocale MSDN
그렇다면 MS는 왜 한 글자당 두 바이트 이상을 사용하는 코드페이지는 예외 처리를 했을까? 이 페이지에 약간의 힌트가 나와 있다. 수많은 레거시 코드가 멀티바이트에서 한 글자에 사용할 수 있는 저장 공간이 최대 두 바이트라고 가정하고 작성했기 때문이란다. 말 그대로 레거시 코드와의 호환성을 위해서 MS는 setlocale로 UTF-8을 지정할 수 없도록 만들었다. 미래 세대의 불편함 따위보다는 과거 세대와의 호환성 유지가 더 중요했기 때문이리라…