Microsoft MVP성태의 닷넷 이야기
C/C++: 179. C++ - _O_WTEXT, _O_U16TEXT, _O_U8TEXT의 Unicode stream 모드 [링크 복사], [링크+제목 복사],
조회: 5372
글쓴 사람
정성태 (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)
13667정성태7/7/20246601닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)파일 다운로드1
13666정성태7/7/20247682Linux: 74. C++ - Vsock 예제 (Hyper-V Socket 연동)파일 다운로드1
13665정성태7/6/20247853Linux: 73. Linux 측의 socat을 이용한 Hyper-V 호스트와의 vsock 테스트파일 다운로드1
13663정성태7/5/20247469닷넷: 2272. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)의 VMID Wildcards 유형파일 다운로드1
13662정성태7/4/20247473닷넷: 2271. C# - WSL 2 VM의 VM ID를 알아내는 방법 - Host Compute System API파일 다운로드1
13661정성태7/3/20247390Linux: 72. g++ - 다른 버전의 GLIBC로 소스코드 빌드
13660정성태7/3/20247499오류 유형: 912. Visual C++ - Linux 프로젝트 빌드 오류
13659정성태7/1/20247838개발 환경 구성: 715. Windows - WSL 2 환경의 Docker Desktop 네트워크
13658정성태6/28/20248212개발 환경 구성: 714. WSL 2 인스턴스와 호스트 측의 Hyper-V에 운영 중인 VM과 네트워크 연결을 하는 방법 - 두 번째 이야기
13657정성태6/27/20247892닷넷: 2270. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)을 위한 EndPoint 사용자 정의
13656정성태6/27/20248052Windows: 264. WSL 2 VM의 swap 파일 위치
13655정성태6/24/20247830닷넷: 2269. C# - Win32 Resource 포맷 해석파일 다운로드1
13654정성태6/24/20247781오류 유형: 911. shutdown - The entered computer name is not valid or remote shutdown is not supported on the target computer.
13653정성태6/22/20247916닷넷: 2268. C# 코드에서 MAKEINTREOURCE 매크로 처리
13652정성태6/21/20249229닷넷: 2267. C# - Linux 환경에서 (Reflection 없이) DLL AssemblyFileVersion 구하는 방법파일 다운로드2
13651정성태6/19/20248466닷넷: 2266. C# - (Reflection 없이) DLL AssemblyFileVersion 구하는 방법파일 다운로드1
13650정성태6/18/20248389개발 환경 구성: 713. "WSL --debug-shell"로 살펴보는 WSL 2 VM의 리눅스 환경
13649정성태6/18/20247943오류 유형: 910. windbg - !py 확장 명령어 실행 시 "failed to find python interpreter" (2)
13648정성태6/17/20248258오류 유형: 909. C# - DynamicMethod 사용 시 System.TypeAccessException
13647정성태6/16/20249310개발 환경 구성: 712. Windows - WSL 2의 네트워크 통신 방법 - 세 번째 이야기 (같은 IP를 공유하는 WSL 2 인스턴스) [1]
13646정성태6/14/20247732오류 유형: 908. Process Explorer - "Error configuring dump resources: The system cannot find the file specified."
13645정성태6/13/20248196개발 환경 구성: 711. Visual Studio로 개발 시 기본 등록하는 dev tag 이미지로 Docker Desktop k8s에서 실행하는 방법
13644정성태6/12/20248843닷넷: 2265. C# - System.Text.Json의 기본적인 (한글 등에서의) escape 처리 [1]
13643정성태6/12/20248294오류 유형: 907. MySqlConnector 사용 시 System.IO.FileLoadException 오류
13642정성태6/11/20248190스크립트: 65. 파이썬 - asgi 버전(2, 3)에 따라 달라지는 uvicorn 호스팅
13641정성태6/11/20248652Linux: 71. Ubuntu 20.04를 22.04로 업데이트
1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...