Microsoft MVP성태의 닷넷 이야기
C/C++: 179. C++ - _O_WTEXT, _O_U16TEXT, _O_U8TEXT의 Unicode stream 모드 [링크 복사], [링크+제목 복사],
조회: 4385
글쓴 사람
정성태 (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)
13901정성태3/9/20251181Windows: 280. Hyper-V의 3가지 Thread Scheduler (Classic, Core, Root)
13900정성태3/8/20251232스크립트: 72. 파이썬 - SQLAlchemy + oracledb 연동
13899정성태3/7/20251199스크립트: 71. 파이썬 - asyncio의 ContextVar 전달
13898정성태3/5/20251205오류 유형: 948. Visual Studio - Proxy Authentication Required: dotnetfeed.blob.core.windows.net
13897정성태3/5/20251210닷넷: 2326. C# - PowerShell과 연동하는 방법 (두 번째 이야기)파일 다운로드1
13896정성태3/5/20251288Windows: 279. Hyper-V Manager - VM 목록의 CPU Usage 항목이 항상 0%로 나오는 문제
13895정성태3/4/20251375Linux: 117. eBPF / bpf2go - Map에 추가된 요소의 개수를 확인하는 방법
13894정성태2/28/20251433Linux: 116. eBPF / bpf2go - BTF Style Maps 정의 구문과 데이터 정렬 문제
13893정성태2/27/20251469Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
13892정성태2/24/20251558닷넷: 2325. C# - PowerShell과 연동하는 방법파일 다운로드1
13891정성태2/23/20251506닷넷: 2324. C# - 프로세스의 성능 카운터용 인스턴스 이름을 구하는 방법파일 다운로드1
13890정성태2/21/20251502닷넷: 2323. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(Win32 API)파일 다운로드1
13889정성태2/20/20251699닷넷: 2322. C# - 프로세스 메모리 중 Private Working Set 크기를 구하는 방법(성능 카운터, WMI) [1]파일 다운로드1
13888정성태2/17/20251491닷넷: 2321. Blazor에서 발생할 수 있는 async void 메서드의 부작용
13887정성태2/17/20251449닷넷: 2320. Blazor의 razor 페이지에서 code-behind 파일로 코드를 분리하는 방법
13886정성태2/15/20251732VS.NET IDE: 196. Visual Studio - Code-behind처럼 cs 파일을 그룹핑하는 방법
13885정성태2/14/20251736닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
13884정성태2/13/20251958닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용파일 다운로드1
13883정성태2/12/20251923닷넷: 2317. C# - Memory Mapped I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13882정성태2/10/20252036스크립트: 70. 파이썬 - oracledb 패키지 연동 시 Thin / Thick 모드
13881정성태2/7/20252155닷넷: 2316. C# - Port I/O를 이용한 PCI Configuration Space 정보 열람파일 다운로드1
13880정성태2/5/20251986오류 유형: 947. sshd - Failed to start OpenSSH server daemon.
13879정성태2/5/20252273오류 유형: 946. Ubuntu - N: Updating from such a repository can't be done securely, and is therefore disabled by default.
13878정성태2/3/20252132오류 유형: 945. Windows - 최대 절전 모드 시 DRIVER_POWER_STATE_FAILURE 발생 (pacer.sys)
13877정성태1/25/20252298닷넷: 2315. C# - PCI 장치 열거 (레지스트리, SetupAPI)파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...