Microsoft MVP성태의 닷넷 이야기
C/C++: 179. C++ - _O_WTEXT, _O_U16TEXT, _O_U8TEXT의 Unicode stream 모드 [링크 복사], [링크+제목 복사],
조회: 5374
글쓴 사람
정성태 (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)
13641정성태6/11/20248655Linux: 71. Ubuntu 20.04를 22.04로 업데이트
13640정성태6/10/20248820Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
13639정성태6/8/20248426오류 유형: 906. C# MAUI - Android Emulator에서 "Waiting For Debugger"로 무한 대기
13638정성태6/8/20248513오류 유형: 905. C# MAUI - 추가한 layout XML 파일이 Resource.Layout 멤버로 나오지 않는 문제
13637정성태6/6/20248440Phone: 20. C# MAUI - 유튜브 동영상을 MediaElement로 재생하는 방법
13636정성태5/30/20248078닷넷: 2264. C# - 형식 인자로 인터페이스를 갖는 제네릭 타입으로의 형변환파일 다운로드1
13635정성태5/29/20248930Phone: 19. C# MAUI - 안드로이드 "Share" 대상으로 등록하는 방법
13634정성태5/24/20249408Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [1]
13633정성태5/22/20248931스크립트: 64. 파이썬 - ASGI를 만족하는 최소한의 구현 코드
13632정성태5/20/20248554Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
13631정성태5/19/20249311Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법 [1]
13630정성태5/19/20248861닷넷: 2263. C# - Thread가 Task보다 더 빠르다는 어떤 예제(?)
13629정성태5/18/20249149개발 환경 구성: 710. Android - adb.exe를 이용한 파일 전송
13628정성태5/17/20248538개발 환경 구성: 709. Windows - WHPX(Windows Hypervisor Platform)를 이용한 Android Emulator 가속
13627정성태5/17/20248599오류 유형: 904. 파이썬 - UnicodeEncodeError: 'ascii' codec can't encode character '...' in position ...: ordinal not in range(128)
13626정성태5/15/20248857Phone: 15. C# MAUI - MediaElement Source 경로 지정 방법파일 다운로드1
13625정성태5/14/20248916닷넷: 2262. C# - Exception Filter 조건(when)을 갖는 catch 절의 IL 구조
13624정성태5/12/20248712Phone: 14. C# - MAUI에서 MediaElement 사용파일 다운로드1
13623정성태5/11/20248406닷넷: 2261. C# - 구글 OAuth의 JWT (JSON Web Tokens) 해석파일 다운로드1
13622정성태5/10/20249195닷넷: 2260. C# - Google 로그인 연동 (ASP.NET 예제)파일 다운로드1
13621정성태5/10/20248636오류 유형: 903. IISExpress - Failed to register URL "..." for site "..." application "/". Error description: Cannot create a file when that file already exists. (0x800700b7)
13620정성태5/9/20248540VS.NET IDE: 190. Visual Studio가 node.exe를 경유해 Edge.exe를 띄우는 경우
13619정성태5/7/20248851닷넷: 2259. C# - decimal 저장소의 비트 구조파일 다운로드1
13618정성태5/6/20248647닷넷: 2258. C# - double (배정도 실수) 저장소의 비트 구조파일 다운로드1
13617정성태5/5/20249469닷넷: 2257. C# - float (단정도 실수) 저장소의 비트 구조파일 다운로드1
13616정성태5/3/20248614닷넷: 2256. ASP.NET Core 웹 사이트의 HTTP/HTTPS + Dual mode Socket (IPv4/IPv6) 지원 방법파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...