実現したいこと
MINGW64で作ったC++プログラムで正しく日本語を扱いたい
前提
- MSYS2-MINGW64使用(※)
- MSVCのRuntimeを使用する
- libstdc++を使用する
- gccを使用する(現状12.2.0)
- MSYS2などの環境に依存せず、作成したバイナリは単品で動作する
- Windows 10以降はmustで、可能ならそれ以前でも正しく動作させたい
- MSYS2環境のbashで動かす場合、環境変数LC_*/LANG/LANGUAGEは未設定とする(LC_CTYPE=ja_JP.UTF-8は可)
- 端末(conhost/mintty/etc...)は何でもいいし、適宜設定は変更するものとするが、原則デフォルトの状態で動作すること
- 日本語Windowsの場合、システムロケールのコードページは932でも65002でもどちらでも動作するものする
- 紐付けられた端末があり、端末への出力が実際にある場合、端末のコードページには追従してもいいし、しなくてもいいが、システムロケールのコードページと端末のコードページのいずれかで入出力できていること
※参考
https://www.msys2.org/docs/environments/
質問
MINGW64で日本語を扱うプログラムをC++で正しく記述する方法を教えてください。
何も考えずに実装すると、Hello, worldレベルで正しく動きません。
発生している問題・エラーメッセージ
例を2つほど挙げます
※ソースコードは次節参照
(1) std::coutに出力するケース
ビルドと実行(システムロケールはコードページ932でmsys2環境(mintty)で実施)
bash
1$ g++ -g -Wall -static hello.cpp -o hello 2$ env | grep LANG 3$ env | grep LC 4LC_CTYPE=ja_JP.UTF-8 5$ ./hello 6縺薙s縺ォ縺。縺ッ 7 8$
msys2環境のminttyの設定はja_JPのUTF-8にしようが、defaultのままにしようが、同じです。
一言で言えば、この状態からcmd /c "dir"したときに日本語が化けないのと同様の、CP932→UTF-8の変換が「どこかで」かかっている文字の化け方をしています。
(2) std::wcoutに出力するケース
bash
1$ g++ -g -Wall -static hellow.cpp -o hellow 2$ ./hellow 3 4$
これは端末以前に何も出力されていないことを示しています。(実際にパイプで繋いでも何も出ていません。)
該当のソースコード
hello.cpp(utf-8で保存)
C++
1#include <iostream> 2int main() { 3 std::cout << "こんにちは" << std::endl; 4 return 0; 5}
hellow.cpp(utf-8で保存)
C++
1#include <iostream> 2int main() { 3 std::wcout << L"こんにちは" << std::endl; 4 return 0; 5}
試したこと
(1) hexdumpで調べる
bash
1$ ./hello | hexdump -C 200000000 e3 81 93 e3 82 93 e3 81 ab e3 81 a1 e3 81 af 0d |................| 300000010 0a |.| 400000011 5 6$ ./hellow | hexdump -C 7 8$
MSYS2 bashでパイプで出力する場合、./helloはUTF-8で出力されている
MSYS2 bashでパイプで出力する場合、./hellowは何も出力しない
(2) iconvで調べる
bash
1$ ./hello | iconv -f utf-8 -t cp932 2▒▒▒▒ɂ▒▒▒ 3 4$ ./hello | iconv -f utf-8 -t cp932 | hexdump -C 500000000 82 b1 82 f1 82 c9 82 bf 82 cd 0d 0a |............| 60000000c 7 8$
iconvでシフトJISに変換出力してみたが、さらに文字化けするだけだった
hexdumpの結果を見る限り変換は正しく実施されている
(3) catを噛ませてみる
bash
1$ ./hello | cat 2こんにちは 3 4$./hello | cat | hexdump -C 500000000 e3 81 93 e3 82 93 e3 81 ab e3 81 a1 e3 81 af 0d |................| 600000010 0a |.| 700000011 8 9$ env | egrep -E '(LC_|LANG)' 10LC_CTYPE=ja_JP.UTF-8 11 12$
MSYS2のcatを噛ませると、同じ内容(hexdumpの結果からUTF-8と分かる)でも「なぜか」正しく表示される。
bash/minttyへの出力方法が./helloとcatで異なっていて、その区別から./helloだけCP932→UTF-8変換をかけられていると推測する。
(4) cmdから見てみる
cmd
1C:\>hello 2縺薙s縺ォ縺。縺ッ 3 4C:\>hello | C:\msys64\usr\bin\cat 5こんにちは 6 7C:\>chcp 8現在のコード ページ: 932 9 10C:\>set | findstr LC_ 11 12C:\>set | findstr LANG 13 14C:\>
※conhost.exe上のcmd.exe(minttyなどMSYS2環境ではなく、純粋なWindows環境)
コマンドプロンプトからでも(3)の現象は同様に再現する。また環境変数の影響でもなく、cmdは子プロセスに変換などの細工をしないので、helloは素直にUTF-8で出力していることが確定し、catが単体でUTF-8→CP932の変換をしているということが分かる。
(5) 絵文字を出力させてみる
bash
1$ cat emoji.cpp 2#include <iostream> 3int main() { 4 std::cout << "😊" << std::endl; 5 return 0; 6} 7 8$ g++ -g -Wall -static emoji.cpp -o emoji 9 10$ ./emoji 11・ 12 13$ ./emoji | cat 14😊 15 16$ ./emoji | cat | hexdump -C 1700000000 f0 9f 98 8a 0d 0a |......| 1800000006 19 20$
絵文字を表示可能なことから、シフトJISで表示しようとしていないことが分かる。さらに具体的に言えば、bashやcatがパイプからの読み込み時に変換をかけていないこと、受け取ったminttyも変換していないことが分かる。
(6) リテラルをシフトJISとしてコンパイルさせる
bash
1$ g++ -g -Wall -static -fexec-charset=CP932 hello.cpp -o hello_sjis 2 3$ ./hello_sjis 4こんにちは 5 6$ ./hello_sjis | hexdump -C 700000000 82 b1 82 f1 82 c9 82 bf 82 cd 0d 0a |............| 80000000c 9 10$
シフトJISとして出力されており、正しく表示されている(minttyはロケールCの文字セットUTF-8)。
(7) 依存DLLを調べる
bash
1$ ldd hello 2 ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7fffc4950000) 3 KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7fffc4660000) 4 KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7fffc2400000) 5 msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7fffc45c0000) 6 7$ ldd `which cat` 8 ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7fffc4950000) 9 KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7fffc4660000) 10 KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7fffc2400000) 11 msys-intl-8.dll => /usr/bin/msys-intl-8.dll (0x430b30000) 12 msys-2.0.dll => /usr/bin/msys-2.0.dll (0x180040000) 13 msys-iconv-2.dll => /usr/bin/msys-iconv-2.dll (0x5603f0000) 14 15$ ldd `which bash` 16 ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7fffc4950000) 17 KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7fffc4660000) 18 KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7fffc2400000) 19 USER32.dll => /c/WINDOWS/System32/USER32.dll (0x7fffc3a60000) 20 win32u.dll => /c/WINDOWS/System32/win32u.dll (0x7fffc2970000) 21 msys-2.0.dll => /usr/bin/msys-2.0.dll (0x180040000) 22 GDI32.dll => /c/WINDOWS/System32/GDI32.dll (0x7fffc32e0000) 23 gdi32full.dll => /c/WINDOWS/System32/gdi32full.dll (0x7fffc20c0000) 24 msvcp_win.dll => /c/WINDOWS/System32/msvcp_win.dll (0x7fffc2360000) 25 ucrtbase.dll => /c/WINDOWS/System32/ucrtbase.dll (0x7fffc2260000) 26 27$
catやbashなどはmsys-2.0.dllをリンクしており、MSYS2ランタイムで動作しているのが分かる。
helloはMINGW64プログラムなのでMSYS2ランタイムで動作していない。
以上から標準入出力の繋がり方が、MSYS2ランタイムを使ったもの同士とそれ以外で異なるのではないかと推測している。
パイプの場合はbashとcmdで違うのではないかと推測している。
(8) wcoutの設定
bash
1$ cat hellow2.cpp 2#include <iostream> 3int main() { 4 std::wcout.imbue(std::locale("")); 5 std::wcout << L"こんにちは" << std::endl; 6 return 0; 7} 8 9$ g++ -g -Wall -static hellow2.cpp -o hellow2 10 11$ ./hellow2.exe 12terminate called after throwing an instance of 'std::runtime_error' 13 what(): locale::facet::_S_create_c_locale name not valid 14 15$
システムロケールへの設定はできない→wcoutは使えない
(https://stackoverflow.com/a/20181564)。
より正確にはwcoutは日本語の出力ができない。libstdc++がgnuモデル以外(今回はgeneralモデル)での変換を実装していないため。
(9) 調べたことまとめ
上記から、MINGW64で正しく日本語を扱うには、
- wcoutを使わず、coutを使う
- msvcrtかWindowsAPIか外部ライブラリなどを使って自分でエンコーディングを(システムロケールのコードページか端末のコードページに)変換する
- リテラルをマルチバイトの固定エンコーディングにする(シフトJISなど)とワイド文字経由のコードページ変換が2度必要になり、手間がかかるため、ワイド文字列のリテラルか、u8プレフィックス付きを使うべき(ただしC++20でchar型での扱いができなくなる)
なのかと思う
ただマルチバイト(cout)にすると、
- 入出力時に変換が必要になり、わざわざマルチバイトにして出力した文字列をWin32側でまたワイド文字列に戻すなどの処理が入りそう
これはそもそもWriteConsoleWがワイド文字列を受け取れるから。
普段使いしてる人の正しい日本語の扱い方を知りたい
補足情報(FW/ツールのバージョンなど)
msys/msys2-runtime 3.4.6-2
下記のような回答は推奨されていません。
このような回答には修正を依頼しましょう。