Microsoft MVP성태의 닷넷 이야기
C/C++: 179. C++ - _O_WTEXT, _O_U16TEXT, _O_U8TEXT의 Unicode stream 모드 [링크 복사], [링크+제목 복사],
조회: 1634
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 3개 있습니다.)
C/C++: 178. C++ - 파일에 대한 Text 모드의 "translated" 동작
; https://www.sysnet.pe.kr/2/0/13766

C/C++: 179. C++ - _O_WTEXT, _O_U16TEXT, _O_U8TEXT의 Unicode stream 모드
; https://www.sysnet.pe.kr/2/0/13768

C/C++: 180. C++ - 고수준 FILE I/O 함수에서의 Unicode stream 모드(_O_WTEXT, _O_U16TEXT, _O_U8TEXT)
; https://www.sysnet.pe.kr/2/0/13776




C++ - _O_WTEXT, _O_U16TEXT, _O_U8TEXT의 Unicode stream 모드

지난 글에서, text 모드가 가진 translated 특징을 알아봤는데요,

C++ - 파일에 대한 Text 모드의 "translated" 동작
; https://www.sysnet.pe.kr/2/0/13766

이번에는 _O_WTEXT, _O_U16TEXT, _O_U8TEXT 3가지 모드에서 지원하는 "Unicode mode"를 설명해 보겠습니다. (_O_TEXT는 ANSI text mode입니다.)




3가지 모드 중에, 우선 _O_U8TEXT 옵션을 먼저 예로 들겠습니다. 아래의 코드는 해당 옵션을 적용해 test_utf8_bom_auto.txt 파일을 생성하는데요,

{
    int fd = 0;
    _sopen_s(&fd, "test_utf8_bom_auto.txt", _O_CREAT | _O_TRUNC | _O_RDWR 
        | _O_U8TEXT, _SH_DENYNO, _S_IREAD | _S_IWRITE);

    _close(fd); // 열기만 하고 닫았음에도 파일의 크기가 3바이트가 됩니다.
}

_sopen_s 함수의 호출 인자에 _O_U8TEXT 모드를 사용했기 때문에, 함수가 실행되자마자 파일에는 UTF-8 BOM이 기록됩니다.

c:\temp> powershell Format-Hex -Path test_utf8_bom_auto.txt

           Path: C:\temp\test_utf8_bom_auto.txt

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000   EF BB BF                                         

이렇게 유니코드 모드로 열린 파일은 재미있는 특징이 하나 있는데요, 반드시 UTF-16 인코딩으로 데이터 I/O를 해야 한다는 제약이 있습니다. 이를 위해 사용되는 현실적인 방법은 wchar_t 타입의 데이터를 사용하는 건데요,

// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen#unicode-support

When a file is opened in Unicode mode, input functions translate the data that's read from the file into UTF-16 data stored as type wchar_t. Functions that write to a file opened in Unicode mode expect buffers that contain UTF-16 data stored as type wchar_t.


(_O_U8TEXT 모드의 경우) 입력 시에는 파일에 있던 utf-8 글자가 wchar_t로 변환되면서 UTF-16 인코딩이 되고, 출력 시에는 UTF-16 인코딩 상태의 wchar_t 데이터가 UTF-8로 변환돼 기록이 되는 식입니다. 예를 들어, 다음과 같이 wchar_t로 데이터를 쓰면,

{
    int fd = 0;
    _sopen_s(&fd, "test_utf8_bom_auto.txt", _O_CREAT | _O_TRUNC | _O_RDWR | _O_U8TEXT, _SH_DENYNO, _S_IREAD | _S_IWRITE);

    const wchar_t* text = L"\xD803\xDC80test한\n"; // 0xd803, 0xdc80: U+10C80 문자의 UTF-16 인코딩 값
    _write(fd, text, wcslen(text) * sizeof(wchar_t));
    _close(fd);
}

파일에는 이런 식으로 바이트가 저장됩니다.

00000000   EF BB BF F0 90 B2 80 74 65 73 74 ED 95 9C 0D 0A  ð²testí..

// 0xef, 0xbb, 0xbf: UTF-8 BOM (_O_U8TEXT로 인해 _sopen_s 호출 시점에 기록됨)

// 0xf0, 0x90, 0xb2, 0x80: U+10C80 문자 (UTF-8로 인코딩된 값)
// 0x74, 0x65, 0x73, 0x74: "test"
// 0xed, 0x95, 0x9c: "한" U+d55c (UTF-8로 인코딩된 값)

// 0x0d, 0x0a: CRLF (translated 모드이기 때문에 \n이 CR-LF로 변환됨)

물론, char로 데이터를 쓰는 것도 가능합니다. 단지 그런 경우에는 wchar_t에 해당하는 2바이트 내용을 그대로 맞춰줘야 합니다. 만약 그렇지 않고 char 데이터를 쓰면,

{
    int fd = 0;
    _sopen_s(&fd, "test_utf8_bom_auto_char.txt", _O_CREAT | _O_TRUNC | _O_RDWR| _O_U8TEXT, _SH_DENYNO, _S_IREAD | _S_IWRITE);

    const char* text = "test1"; // (2바이트 정렬이 아닌) 5바이트이므로,
    _write(fd, text, strlen(text)); // crash 발생
    _close(fd);
}

디버그 모드로 실행 시 다음과 같은 assertion 에러가 발생합니다. (릴리스 모드로 실행하면 이벤트 로그에 crash 기록이 남습니다.)

Debug Assertion Failed!

Program: ...u8text_u16text\unicode_mode\x64\Debug\ConsoleApplication1.exe
File: minkernel\crts\ucrt\src\appcrt\lowio\write.cpp
Line: 659

Expression: buffer_size % 2 == 0

설령 저 오류를 피하기 위해 억지로 2바이트 정렬을 맞춘다고 해도,

const char* text = "test";
_write(fd, text, strlen(text)); // 4바이트, 비정상 데이터 출력
_close(fd);

출력 파일을 보면 우리가 원하는 "test"가 아닌 엉뚱한 데이터를 확인할 수 있습니다.

00000000   EF BB BF E6 95 B4 E7 91 B3                       æ´ç³

// 0xef, 0xbb, 0xbf: UTF-8 BOM (_O_U8TEXT로 인해 _sopen_s 시점에 기록됨)

// 0xe6, 0x95, 0xb4: "te" (UTF-8로 인코딩된 값)
// 0xe7, 0x91, 0xb3: "st" (UTF-8로 인코딩된 값)

왜냐하면, (_O_U8TEXT로 인해) unicode 모드로 동작하는 _write는 입력 데이터를 wide character로 간주해 UTF-16LE로 인코딩된 문자라고 보기 때문입니다. 즉, "te"에 해당하는 '0x74 0x65' 2바이트 데이터를 'U+6574' 문자라고 여기고 그에 해당하는 UTF-8 인코딩 값인 '0xe6 0x95 0xb4'를 출력하는 식입니다.




파일은 생성된 이후에도 _setmode 함수를 이용해 모드를 변경할 수 있습니다. 예를 들어, 이전의 코드를 다음과 같이 변경하는 것도 가능한데요,

int fd = 0;

// 생성 시점에 BOM을 출력하지 않음
_sopen_s(&fd, "test_utf8_no_bom.txt", _O_CREAT | _O_TRUNC | _O_RDWR, _SH_DENYNO, _S_IREAD | _S_IWRITE);

_setmode(fd, _O_U8TEXT); // 이후에 바꿨다고 해서 BOM이 추가되지는 않음

const wchar_t* text = L"test";
_write(fd, text, wcslen(text) * sizeof(wchar_t));
_close(fd);

이에 대한 출력 결과를 보면 다음과 같습니다.

00000000   74 65 73 74                                      test

보는 바와 같이 "BOM(0xef, 0xbb, 0xbf)" 없이 곧바로 "test"에 해당하는 UTF-8 인코딩 값이 나오는데요, 만약 저런 경우에 BOM을 추가하고 싶다면 _setmode로 변경하기 전에 char로 BOM을 직접 출력해야 합니다.

int fd = 0;
_sopen_s(&fd, "test_utf8_bom_manual.txt", _O_CREAT | _O_TRUNC | _O_RDWR, _SH_DENYNO, _S_IREAD | _S_IWRITE);

char bom[] = { 0xEF, 0xBB, 0xBF };
_write(fd, bom, sizeof(bom)); // _setmode 변경 전에 BOM을 직접 출력

_setmode(fd, _O_U8TEXT);

const wchar_t* text = L"test";
_write(fd, text, wcslen(text) * sizeof(wchar_t));
_close(fd);

00000000   EF BB BF 74 65 73 74                             test

만약 _setmode 후에 변경하려고 하면 어떻게 될까요? 그런 상황이라면, _setmode로 인해 uncode 모드로 바뀌었기 때문에 3바이트 BOM을 출력하는 경우 이전에 설명했던 제약으로 인해 crash가 발생합니다.

_setmode(fd, _O_U8TEXT); // 유니코드 모드로 변경

char bom[] = { 0xEF, 0xBB, 0xBF };
_write(fd, bom, sizeof(bom)); // 이 시점에 BOM을 출력하면 (2바이트 정렬이 아닌 3바이트이므로) crash 발생

그렇다고 wchar_t로 BOM을 출력하면,

_setmode(fd, _O_U8TEXT);

wchar_t bom[] = { 0xEF, 0xBB, 0xBF };
_write(fd, bom, 3 * sizeof(wchar_t));

이제는, (0x00, 0xEF), (0x00, 0xBB), (0x00, 0xBF)를 한 글자로 인식해 그에 해당하는 UTF-8 인코딩을 하기 때문에 BOM이 아닌 데이터가 출력됩니다.

이 정도면 대충 설명이 끝났군요. 다행히 이런 원칙은 _O_U16TEXT에도 그대로 적용되므로 중복 설명은 생략하겠습니다. 단지, wchar_t가 이미 UTF-16 인코딩 데이터를 담고 있으므로 _O_U16TEXT 모드로 열린 파일에 대한 출력은 딱히 변환 단계를 거치지 않고 곧바로 출력이 된다는 정도만 알아두시면 되겠습니다.

참고로, _O_U8TEXT, _O_U16TEXT 옵션으로 파일을 열 때는, 단지 힌트로만 작용을 할 뿐 우선순위는 파일에 기록된 BOM이 더 높습니다. 즉, 파일의 BOM이 UTF-16LE(0xff, oxfe)라면, 그 파일을 열 때 _O_U8TEXT를 지정한다고 해도 UTF-16LE로 간주합니다.




자, 그렇다면 이제 마지막 남은 _O_WTEXT가 있군요. ^^ 문서상으로는 _O_U16TEXT와 일단 유사한 듯 보이지만,

// https://learn.microsoft.com/en-us/cpp/c-runtime-library/translation-mode-constants#remarks

_O_WTEXT Opens file in UTF-16 text (translated) mode. The wide-character versions of the text translations of _O_TEXT are supported.

_O_U16TEXT Opens file in UTF-16 no BOM text (translated) mode. The wide-character versions of the text translations of _O_TEXT are supported.


실제로 테스트를 해보면 혼란스러운 점이 있습니다. 위의 설명에 따르면 _O_U16TEXT 옵션이 "no BOM" 텍스트 모드로 연다고 돼 있지만 실제로는 (위에서 설명했듯이) BOM을 기본적으로 추가합니다. 반면 _O_WTEXT는 출력 파일을 열었을 때 BOM을 추가하지 않습니다.

이 외에도 _O_WTEXT는 신비한(?) 면들이 있는데요, 예를 들어 BOM이 없는 일반 ascii 파일을,

00000000   74 65 73 74                                      test

_O_WTEXT로 열어 UTF-16 데이터를 추가/출력해 보면,

{
    int fd = 0;
    _sopen_s(&fd, "test_unicode_no_bom.txt", _O_WRONLY | _O_APPEND | _O_WTEXT, _SH_DENYNO, _S_IWRITE);

    const wchar_t* text = L"\xD803\xDC80test한\n"; // 0xd803,0xdc80 == U+10C80
    _write(fd, text, wcslen(text) * sizeof(wchar_t));
    _close(fd);
}

결과가 이렇게 나옵니다.

00000000   74 65 73 74 03 D8 80 DC 74 00 65 00 73 00 74 00  test.ØÜt.e.s.t.
00000010   5C D5 0D 0A 00                                   \Õ...

// 74, 65, 73, 74: 기존에 저장이 돼 있던 "test"
// 03, d8, 80, dc: U+10C80에 해당하는 UTF-16 인코딩 값
// 74, 00, 65, 00, 73, 00, 74, 00: "test"의 UTF-16 인코딩 값
// 5C, d5: "한"의 UTF-16 인코딩 값
// 0d, 0a, 00: CRLF + '\0'

일단, 이전 데이터인 test가 ASCII 문자로 저장돼 있는 상태에서 UTF-16LE로 인코딩된 데이터들이 따라오고 있어 정상인 듯한데요, 문제는 마지막의 "0d 0a 00"입니다. 즉, '\n' 글자가 펼쳐진 것인데 원래 UTF-16LE 인코딩이라면 0x0d, 0x00, 0x0a, 0x00이 되어야 합니다.

또 하나 재미있는 점은, _O_WTEXT로 연 파일을 명시적으로 _O_WTEXT로 모드로 다시 변경해 보면,

{
    int fd = 0;
    _sopen_s(&fd, "test_unicode_no_bom.txt", _O_WRONLY | _O_APPEND | _O_WTEXT, _SH_DENYNO, _S_IWRITE);

    int old_mode = _setmode(fd, _O_WTEXT);

    const wchar_t* text = L"\xD803\xDC80test한\n"; // 0xd803,0xdc80 == U+10C80
    _write(fd, text, wcslen(text) * sizeof(wchar_t));
    _close(fd);
}

출력 결과가 다음과 같이 나옵니다.

00000000   74 65 73 74 03 D8 80 DC 74 00 65 00 73 00 74 00  test.ØÜt.e.s.t.
00000010   5C D5 0D 00 0A 00                                \Õ....

보는 바와 같이 '\n' 글자가 (UTF-16 인코딩에 맞게) 0d, 00, 0a, 00 4바이트로 출력됐습니다. 위에서 또 하나 이상한 점이 있는데요, _setmode는 해당 파일의 이전 모드를 반환값으로 돌려주는데, 재미있게도 old_mode 변수에는 0x4000(_O_TEXT) 값이 들어 있습니다. 하지만, _setmode를 한 번 더 해서 살펴보면,

_sopen_s(&fd, "test_unicode_no_bom2.txt", _O_WRONLY | _O_APPEND | _O_WTEXT, _SH_DENYNO, _S_IWRITE);

int old_mode = _setmode(fd, _O_WTEXT); // old_mode == _O_TEXT (0x4000)
old_mode = _setmode(fd, _O_WTEXT); // old_mode == _O_WTEXT (0x10000)

이번엔 old_mode에 0x10000(_O_WTEXT) 값이 들어 있습니다. 정말 혼란스럽죠? ^^; 이것이 버그인지, 의도한 것인지는 알 수 없으나 암튼 저런 결과로 인해 개인적으로는 _O_WTEXT를 사용하지 않는 것을 권장합니다. (혹시, _O_WTEXT 옵션에 대해 깔끔하게 설명해 주실 분 계실까요? ^^)




정리하자면, _O_U8TEXT, _O_U16TEXT 옵션은 대상이 되는 파일의 데이터에 대한 인코딩을 나타낸다는 점을 기억하시기 바랍니다. 즉, 코드에서 wchar_t로 넘겨주는 데이터는 UTF-16LE로 인코딩된 것이고 이것을 대상 파일에 설정한 _O_U8TEXT, _O_U16TEXT 옵션에 따라 변환해 기록하는 것입니다. 반대로 읽는 동작에서는, 파일에 있는 UTF-8/UTF-16LE 데이터를 읽어서 UTF-16LE로 변환한 다음 그것을 wchar_t 변수에 담아줍니다.

이와 유사한 이야기를 전에 C#으로도 한 번 설명한 적이 있습니다.

C# 문자열의 인코딩이란?
; https://www.sysnet.pe.kr/2/0/1461

C#도 string 변수 자체에는 UTF-16LE 인코딩으로 데이터를 보관하고 있지만, 대상 파일에 쓸 때는 지정한 인코딩에 따라 변환시키는 과정을 거치고, 읽는 작업도 마찬가지로 파일의 인코딩된 데이터를 string 변수에 담을 때는 UTF-16LE로 변환된 결과를 보관하는 것입니다.




참고로, 아래의 사이트에 접속해,

Compiler Explorer
; https://compiler-explorer.com/

다음의 코드를 입력한 후,

#include <iostream>

int main(int, char*[])
{
    const wchar_t wcstr[] = L"test한";

    std::wcout << wcstr << std::endl;
}

"x64 msvc v19.latest" 탭에 출력된 어셈블리 코드를 보면, wchar_t 문자열이 UTF-16LE로 인코딩돼 있음을 확인할 수 있습니다.

$SG55695 DB     't', 00H, 'e', 00H, 's', 00H, 't', 00H, '\', 0d5H, 00H, 00H
...[생략]...

"한" 글자가 '\', 0d5H로 인코딩돼 있는데요, 각각 0x5c, 0xd5로 UTF-16LE 인코딩 값에 해당합니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




지금까지 설명한 것이 좀 복잡한 듯한데요, 그래도 Win32 API를 직접 사용하는 예제에 비하면,

Conventional wisdom is retarded, aka What the @#%&* is _O_U16TEXT?
; https://archives.miloush.net/michkap/archive/2008/03/18/8306597.html

C/C++ 표준 라이브러리 사용이 꽤나 편리하다는 것을 알 수 있습니다. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 10/28/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13814정성태11/13/20241215닷넷: 2311. C# - Windows / Linux 환경에서 Native Thread ID 가져오기파일 다운로드1
13813정성태11/12/20241076닷넷: 2310. .NET의 Rune 타입과 emoji 표현파일 다운로드1
13812정성태11/11/2024963오류 유형: 933. Active Directory - The forest functional level is not supported.
13811정성태11/11/20241003Linux: 104. Linux - COLUMNS 환경변수가 언제나 80으로 설정되는 환경
13810정성태11/10/20241101Linux: 103. eBPF (bpf2go) - Tracepoint를 이용한 트레이스 (BPF_PROG_TYPE_TRACEPOINT)
13809정성태11/10/20241052Windows: 271. 윈도우 서버 2025 마이그레이션
13808정성태11/9/20241116오류 유형: 932. Linux - 커널 업그레이드 후 "error: bad shim signature" 오류 발생
13807정성태11/9/20241153Linux: 102. Linux - 커널 이미지 파일 서명 (Ubuntu 환경)
13806정성태11/8/20241225Windows: 270. 어댑터 상세 정보(Network Connection Details) 창의 내용이 비어 있는 경우
13805정성태11/8/20241121오류 유형: 931. Active Directory의 adprep 또는 복제가 안 되는 경우
13804정성태11/7/20241207Linux: 101. eBPF 함수의 인자를 다루는 방법
13803정성태11/7/20241395닷넷: 2309. C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도
13802정성태11/6/20241631Windows: 269. GetSystemTimeAsFileTime과 GetSystemTimePreciseAsFileTime의 차이점파일 다운로드1
13801정성태11/5/20241585Linux: 100. eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE)
13800정성태11/3/20241911닷넷: 2308. C# - ICU 라이브러리를 활용한 문자열의 대소문자 변환 [2]파일 다운로드1
13799정성태11/2/20241566개발 환경 구성: 732. 모바일 웹 브라우저에서 유니코드 문자가 표시되지 않는 경우
13798정성태11/2/20241604개발 환경 구성: 731. 유니코드 - 출력 예시 및 폰트 찾기
13797정성태11/1/20241709C/C++: 185. C++ - 문자열의 대소문자를 변환하는 transform + std::tolower/toupper 방식의 문제점파일 다운로드1
13796정성태10/31/20241571C/C++: 184. C++ - ICU dll을 이용하는 예제 코드 (Windows)파일 다운로드1
13795정성태10/31/20241540Windows: 268. Windows - 리눅스 환경처럼 공백으로 끝나는 프롬프트 만들기
13794정성태10/30/20241649닷넷: 2307. C# - 윈도우에서 한글(및 유니코드)을 포함한 콘솔 프로그램을 컴파일 및 실행하는 방법
13793정성태10/28/20241551C/C++: 183. C++ - 윈도우에서 한글(및 유니코드)을 포함한 콘솔 프로그램을 컴파일 및 실행하는 방법
13792정성태10/27/20241415Linux: 99. Linux - 프로세스의 실행 파일 경로 확인
13791정성태10/27/20241474Windows: 267. Win32 API의 A(ANSI) 버전은 DBCS를 사용할까요?파일 다운로드1
13790정성태10/27/20241535Linux: 98. Ubuntu 22.04 - 리눅스 커널 빌드 및 업그레이드
13789정성태10/27/20241457Linux: 97. menuconfig에 CONFIG_DEBUG_INFO_BTF, CONFIG_DEBUG_INFO_BTF_MODULES 옵션이 없는 경우
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...