아래의 글은 Jeffrey Richter 의 Programming Applications for MS Windows 4th edition중에 Part 1의 2장 Unicode 의 How to Write Unicode Source Code 부분을 번역해두었던 것을 올리는 것이다. (요즘 포스트가 뜸해서^^;;)
국내에 번역본이 출간된 걸로 알고 있지만 절판된 관계로 구해보지 못했고, 출판사도 망한 것으로 알고 있으며(다른 곳으로 흡수 합병 된 것인가?) 개인적인 용도로 번역해봤으므로 저작권에 문제가 있다고 생각되는 분이 있다면 리플 달아주세용
---------------------------------------------------------------------------------
마이크로소프트는 윈도우 API를 유니코드를 고려해 디자인하였으므로 유니코드는 프로그램 작성시에 많은 영향을 주지는 않는다. 실제로 하나의 소스코드만으로 유니코드를 사용하는 상황이든 아니든 컴파일이 가능하다. 사용자는 단지 두개의 매크로(UNICODE와 _UNICODE)로 유니코드 사용여부를 결정하고 재 컴파일 하면 된다.
C 런타임 라이브러리에서의 유니코드 지원
유니코드 캐릭터 문자열을 사용하기 위해 몇가지 자료형(data type)이 정의되었다. 유니코드의 char 자료형인 wchar_t 를 정의하기 위해 표준 C헤더파일인 string.h 파일이 수정되었다.
typedef unsigned short wchar_t; |
예를 들어, 99개의 캐릭터와 마지막 널 캐릭터로 된 유니코드 문자열을 저장할 버퍼를 생성하고싶으면 다음과 같이 할 수 있다.
wchar_t szBuffer[100]; |
위 문장은 wchar_t가 16비트 자료형이기 때문에 16비트로 100개의 배열을 생성한다. 물론 strcpy, strchr, strcat 같은 표준 C 런타임 문자열 함수들은 8비트 ANSI 문자열에서만 동작하므로 위의 유니코드 문자열을 처리할 수 없다. 그래서 ANSI C또한 유니코드 처리용 함수들을 가지고 있다. Figure 2-1에서 같은 기능을 하는 표준 ANSI C문자열 함수와 유니코드 함수 몇가지를 보여준다.
Figure 2-1. Standard ANSI C 문자열 처리 함수들과 유니코드 버전
char * strcat(char *, const char *); wchar_t * wcscat(wchar_t *, const wchar_t *); char * strchr(const char *, int); wchar_t * wcschr(const wchar_t *, wchar_t); int strcmp(const char *, const char *); int wcscmp(const wchar_t *, const wchar_t *); char * strcpy(char *, const char *); wchar_t * wcscpy(wchar_t *, const wchar_t *); size_t strlen(const char *); size_t wcslen(const wchar_t *); |
모든 유니코드 함수 이름 앞에 wcs가 붙어 있는 것이 보인다. 이것은 wide character string의 약자로 유니코드 함수를 사용하기 위해선 ANSI문자열 함수앞의 str접두사를 단지 wcs로만 바꾸면 된다.
NOTE 개발자들이 간과하는 중요한 점이 하나 있는데, MS가 제공하는 C런타임 라이브러리들은 모두 ANSI 표준을 따른다는 것이다. ANSI C는 C런타임 라이브러리가 유니코드 캐릭터와 문자열을 지원하도록 요구하고 있다. 이 것은 유니코드 캐릭터와 문자열을 다루기 위해서 언제나 C런타임 라이브러리 함수를 호출할 수 있다는 것을 의미한다. 다시 말해 윈도우98이라도 wcscat, wcslen, wcstok같은 함수들은 잘 작동한다. |
ANSI문자열과 유니코드 문자열을 위한 str함수나 wcs함수들을 소스코드상에 직접 호출하게 프로그래밍을 하게 되면 나중에 쉽게 컴파일하지 못하게 된다. 이 글 첫 머리에 하나의 소스코드로 ANSI문자열과 유니코드 문자열 모두를 지원하도록 컴파일 할 수 있다고 했는데, 이걸 가능하게 하려면 먼저 string.h 헤더 대신 tchar.h를 include 한다.
tchar.h 헤더는 ANSI와 유니코드에 구애받지 않는 소스코드의 작성을 돕는 단 한가지의 목적을 위해 존재한다. str이나 wcs 함수들을 직접 호출 하는 대신 사용할 수 있는, 매크로들의 집합으로 구성되어 있는데, 소스에 _UNICODE를 정의(define)해 놓으면 매크로들은 wcs함수들을 사용하게 되며 _UNICODE를 정의하지 않으면 str함수들을 내부적으로 사용하도록 되어있다.
예를 들어 tchar.h 에는 _tcscpy라는 매크로가 있다. tchar.h를 include 하면서 _UNICODE를 정의하지 않는다면 _tcscpy 는 ANSI의 strcpy 함수로 바뀐다. _UNICODE가 정의되어 있다면 wcscpy함수로 바뀌게 된다. C런타임의 모든 문자열을 인자로 받는 함수들은 tchar.h 에 매크로가 정의되어 있다. 코드를 작성할 때 ANSI/유니코드의 특정함수들을 직접 호출하는 대신 이러한 매크로들을 사용하게 된다면 ANSI나 유니코드에 구애받지 않고 쉽게 컴파일 되는 코드를 작성할 수 있게 될 것이다.
불행히 이러한 매크로의 사용 이외에 몇가지를 좀 더 해줘야한다. TChar.h 는 추가적인 매크로들을 몇가지 더 포함하고 있다.
ANSI/유니코드에 구애 받지 않는 문자열 배열을 정의하기 위해서는 TCHAR 자료형을 사용해야 한다. _UNICODE가 정의 되어 있다만 TCHAR 타입은 다음처럼 선언된다.
typedef wchar_t TCHAR; |
_UNICODE가 정의되어 있지 않다면TCHAR는 다음처럼 선언된다.
typedef char TCHAR; |
이런 자료형을 사용하면 문자열을 다음처럼 할당할 수 있다.
TCHAR szString[100]; |
문자열의 포인터도 생성할 수 있다.
TCHAR *szError = "Error"; |
그러나 위의 문장은 문제가 하나 있다. MS의 C++컴파일러는 디폴트로 "Error"같은 문자열을 유니코드가 아닌 ANSI스트링으로 간주해 컴파일 하게 된다. 따라서 _UNICODE가 정의되어 있지 않다면 컴파일러는 위의 문장을 제대로 컴파일 하겠지만 _UNICODE가 정의되어 있다면 위 문장은 에러가 된다. 문자열을 ANSI대신 유니코드로 사용하기 위해서는 위의 문장을 다음과 같이 작성해야한다.
TCHAR *szError = L"Error"; |
"Error"문자열 앞에 붙어 있는 대문자 L은 컴파일러에게 해당 문자열을 유니코드로 컴파일 하도록 알려준다. 이렇게 하면 컴파일러는 문자열을 저장할 때 각 문자 사이사이에 0 byte들을 채워 넣어 2바이트로 만들게 된다. 문제는 이제 이 소스코드는 단지 _UNICODE가 정의되어 있을 때만 원하는 대로 컴파일 된다는 점이다. 그래서 이 대문자 L을 위한 또다른 매크로가 준비되어 있는데 바로 _TEXT가 그것이다. 역시 tchar.h 에 정의되어 있다.
_UNICODE가 정의된다면 _TEXT는 다음처럼 정의된다.
#define _TEXT(x) L ## x |
_UNICODE가 정의되어 있지 않다면 아래와 같다.
#define _TEXT(x) x |
이 매크로를 사용해서, 이제 위의 문장을 아래처럼 고치게 되면 _UNICODE 가 정의되든, 정의되지 않던지 올바르게 컴파일 할 수 있게 된다.
TCHAR *szError = _TEXT("Error"); |
_TEXT매크로는 문자리터럴에도 사용할 수 있다. 예를 들어 문자열의 첫번째 문자가 대문자 J인지 확인 하려면 다음과 같은 코드를 작성한다.
if (szError[0] == _TEXT('J')) { // 첫번째 문자가 'J' } else { // 첫번째 문자가 'J'가 아님
} |
Window 헤더 파일들은 다음과 같은 자료형들을 정의한다.
Data Type |
Description |
WCHAR |
유니코드 Char |
PWSTR |
유니코드 문자열 포인터 |
PCWSTR |
상수 유니코드 문자열 포인터 |
위의 자료형들은 항상 유니코드 문자와 문자열들을 참조한다. Window 헤더 파일들은 ANSI/유니코드 자료형인 PTSTR 과 PCTSTR도 정의하고 있다. 이 자료형들은 컴파일 할 때 UNICODE매크로를 정의 여부에 따라 ANISI문자열이나 유니코드 문자열을 나타내게 된다.
UNICODE 매크로 앞에 밑줄 _이 붙지 않은 점에 주의한다. _UNICODE매크로는 C런타임 헤더 파일에서 사용되고, UNICODE매크로는 윈도우 헤더 파일에서 사용된다. 소스코드를 컴파일 할 때 대개는 양쪽 모두 정의 해야 한다.
이전에 CreateWindowEx함수가 두가지가 있다고 언급했던 적이 있다. 유니코드 문자열을 인자로 받는 CreateWindowEx와 ANSI 문자열을 인자로 받는 CreateWindowEx가 있으며 실제로 두 함수는 아래와 같은 함수 원형이 있다.
HWND WINAPI CreateWindowExW( DWORD dwExStyle, PCWSTR pClassName, PCWSTR pWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, PVOID pParam); HWND WINAPI CreateWindowExA( DWORD dwExStyle, PCSTR pClassName, PCSTR pWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, PVOID pParam); |
CreateWindowExW는 CreateWindowEx 의 유니코드 버전이며 이름 끝에 붙은 W는 wide를 뜻한다. 유니코드 캐릭터는 ANSI의 8비트보다 넓은 16비트이므로 wide 캐릭터라고 불린다. CreateWindowExA 함수의 끝에 붙은 대문자 A는 ANSI를 나타낸다.
하지만 일반적으로 코드상에서 CreateWindowEx를 사용하지 CreateWindowExW 나 CreateWindowExA 함수를 직접 호출하지는 않는다. WinUser.h에 보면 CreateWindowEx 는 사실 다음과 같이 정의되어 있다.
#ifdef UNICODE #define CreateWindowEx CreateWindowExW #else #define CreateWindowEx CreateWindowExA #endif // !UNICODE |
컴파일 할 때 UNICODE의 정의여부는 어떤 버전의 CreateWindowEx를 사용할지를 결정한다. 16비트 윈도우 애플리케이션을 포팅중이라면 이야기가 쉽다. 16비트 윈도우는 오로지 ANSI버전의 CreateWindowEx 만 제공하기 때문에 CreateWindowEx를 호출해도 ANSI버전인 CreateWindowExA 으로만 수행된다.
윈도우2000에서는, ANSI용 함수 CreateWindowExA는 단순히 인자로 넘어온 ANSI문자열을 유니코드 문자열로 변환하기 위해 메모리를 할당하는 변환 레이어가 된다, 함수가 호출되면 CreateWindowExW를 호출해서 변환된 문자열을 넘긴다. CreateWindowExW 함수에서 복귀할 때 CreateWindowExA는 할당했던 메모리버퍼를 해제한 후 윈도우 핸들을 리턴한다.
만약 다른 개발자가 사용할 DLL 라이브러리를 작성중이라면 위 방법의 사용을 고려해볼만 하다. DLL에 ANSI버전과 유니코드버젼, 두개의 함수를 제공한다면, ANSI버전에서는 단순히 메모리를 할당해서 문자열을 변환 한 후 유니코드버젼의 함수를 호출하는 것이다. (이 장의 마지막 부분에서 이러한 과정을 보여준다.)
윈도우98에서는 CreateWindowExA쪽이 그 일을 하는 함수다. 윈도우98도 유니코드를 파라미터로 받아들이는 함수를 모두 제공하지만 내부적으로 유니코드를 ANSI문자열로 변환하지는 않는다. 따라서 이들 함수 호출은 실패하며 GetLastError를 호출해보면 ERROR_CALL_NOT_IMPLEMENTED 를 반납한다. 윈도우98에서는 오로지 ANSI버전의 API함수들만이 제대로 동작하며 만약 컴파일된 프로그램이 와이드캐릭터 함수를 하나라도 호출한다면 윈도우98에서는 실행되지 않을 것이다.
윈도우 API중에 WinExec나 OpenFile같은 함수들은 단지 16비트 윈도우 프로그램과의 호환성을 위해서 존재하며 사용을 피해야 한다. WinExec와 OpenFile은 CreateProcess와 CreateFile로 바꿔 써야 한다. 저러한 옛날 함수들은 어쨌든 내부적으론 새로운 함수들을 호출하지만, 가장 큰 문제는 유니코드 문자열을 받지 않는다는 점이다. 저런 함수들을 호출 할 때는 ANSI문자열을 인자로 전달해야 한다. 이는 다르게 말하면 윈도우2000의 새로운 함수들은 모두 ANSI와 유니코드를 지원하는 버전이 둘다 존재한다는 것을 뜻하기도 한다.
윈도우는 또한 여러가지 문자열 함수들을 제공한다. 이 함수들은 strcpy나 wcscpy와 같은 C런타임 함수들과 비슷한데, 다른 점은 윈도우가 제공하는 함수들은 운영체제(OS)의 일부라는 점이며 많은 OS 컴포넌트들이 C런타임 함수 대신 이 윈도우 함수를 사용한다. 필자는 C런타임 문자열 함수보다 OS함수의 사용을 추천한다. OS의 문자열 함수들은 윈도우의 쉘 프로세스나 탐색기 같은 덩치 큰 프로그램에 주로 사용되며, 여기서 문자열 함수들은 아주 빈번하게 사용되어 메모리에 이미 로드되어 있을 확률이 높기 때문에, 제작하는 애플리케이션의 성능을 약간이라도 향상시키는데 도움이 된다.
이러한 함수들을 사용하기 위해서는 윈도우98 이상의 운영체제가 깔려 있어야 하며 4.0 버전 이상의 Internet Explorer 가 설치되어 있다면 그 이전의 윈도우에서도 사용할 수 있다.
전통적인 OS 함수 스타일처럼 OS의 문자열 함수 이름들도 대소문자가 혼합된 모양새를 하고 있다 : StrCat, StrChr, StrCmp, StrCpy… 이 함수들을 사용하기 위해서는 ShlWApi.h헤더파일을 include해줘야 한다. 그리고 전에 논의했 듯이 이 문자열 함수들 또한 StrCatA나 StrCatW같은 ANSI나 유니코드 버전이 된다. 이 함수들은 운영체제 함수이므로 프로그램을 빌드할 때 UNICODE(_UNICODE가 아닌)를 정의하게되면 와이드버전으로 바뀐다.