Windows/VC++ で char を UTF8 で扱う
Windows/VC++ でも徐々に UTF8 対応しやすくなっていますが、過去との互換で Shift-JIS を引きずっていて、他の OS/環境同様に char 型を UTF8 で扱おうとすると少々面倒です。
オプションを設定し、コンソール出力のおまじないをし、さて、ファイルパスをどうしようかと、プログラムで対処……え、manifest を用意すればよい?!
というわけで、Windows10/11 で Visual C++、Visual Studio 2017,2019,2022 で UTF8 を使う場合のメモ書です。
※ VS2015 でも std::filesystem が使えないだけで他は共通だと思います。
1. ソーステキスト&バイナリでの char の UTF8 対応
vc++ のデフォルトでは、ソーステキストの文字エンコードが Unicode か SJIS かに関わらず、char は SJIS(OSの文字エンコード)、wchar_t は UTF16 でバイナリが作られるようになっています。
またソーステキストが UTF8 の場合は BOM 付きでないと SJIS で解釈されて文字化けすることがあります。
コンパイラ・オプションとして
/utf-8 ソース・テキスト&実行バイナリを UTF8 で扱う.
/source-charset:utf-8 ソース・テキストを UTF8 で扱う.
/execution-charset:utf-8 実行バイナリのcharを UTF8 で扱う.
があるので、/utf-8 を指定しておけば、BOM無 UTF8 のソースで問題なく、バイナリも UTF8 で char 文字列作られるようになります。
このオプション指定は、Visual Studio IDE 上の c++ プロジェクトのプロパティでは、専用の設定項目がなさそうなので、
「プロジェクト」 → 「xxxx プロパティページ」 → 「構成プロパティ」 → 「C/C++」 → 「コマンドライン」 → 「追加のオプション」
に /utf-8 を追記することになります。
2. UTF8 でのコンソール出力
2-1. 実行環境で対処
コマンド・プロンプト(PowerShell プロンプト)は日本語 Windows のデフォルトが Code Page 932 (Shift JIS) なので、/utf-8 付で作ったプログラムのコンソール出力は文字化けしてしまいます。
コマンドラインで
> chcp 65001
とし UTF8 環境にすることで、文字化けせずに出力できるようになります。
もしコマンドプロンプトのショートカットや Windows Terminal で UTF8 専用を用意する場合は、
%SystemRoot%\System32\cmd.exe /k chcp 65001
のように /k chcp 65001 を引数に足して設定すればよいでしょう。
ただ、OS 付属のツールは UTF8 対応していますが、SJIS(CP932) 前提で作られた他のツールは当然文字化けしますので、混在して使う場合はどちらにしても注意が必要です。
(バッチの中で chcp 932 と 65001 で使い分けるくらいならSJIS環境ままで大差ないかも)
2-2. プログラム側での UTF8 でのコンソール出力
chcp は、実行するプログラムのコンソール出力の文字エンコードを切り替えているだけのようで、コマンド・プロンプトや Windows Terminal の表示自体は、Unicode で行われています。
Windows API にはコンソール出力の Code Page を切り替える API があり、プログラムが動いている間だけ UTF8 出力にすることができます。(バッチ等でわざわざ chcp する必要がなくなります)
#include <stdio.h>
#include <windows.h>
int main() {
UINT sav = GetConsoleOutputCP();
SetConsoleOutputCP(65001);
printf("はろー❤わーるど\n");
SetConsoleOutputCP(sav);
return 0;
}
SetConsoleOutputCP で UTF8 に切り替えるのですが、プログラムが終了してもずっと設定したままになるので、GetConsoleOutputCP で元の Code Page を控えて、終わるときに戻しています。
これでコマンドプロンプトの Code Page が SJIS,UTF8 に関わらず、実行しても文字化けせず、後続の他の実行にも影響を与えずに済みます。
※既存 SJIS プログラムのUTF8環境対応として、同様な手順で SetConsoleOutputCP(932) を用いるのは手かもしれません。
3. Windows API の UTF8 対応
表示は前記で対処できますが、ファイルパスや main のコマンドライン引数(argv)等、OS とやり取りする char 文字列は SJIS(OSの文字コード) ままなので、UTF8 文字列を用いる場合は対応が必要になります。
古めの Windows だと UTF8文字列を wchar_t(UTF16)文字列に変換して Windows API や MS拡張の標準ライブラリ風関数を用いて対応する必要があるのですが……
すらりん日記:UTF-8文字列をAPI引数で使えるようになった
https://blog.techlab-xe.net/using-string-utf-8-winapi/
Microsoft: Use UTF-8 code pages in Windows apps
https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page
によると、Windows10 1903 から ANSI系 Windows API で UTF8 対応がされているようです。
もちろん内部で ANSI系 Windows API を使っている C標準ライブラリ や C++標準ライブラリも対応になります。
VC++ で使うには、Active Code Page を UTF8 にするための manifest ファイルが必要なようで、Microsoft の頁にかかれている
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity type="win32" name="..." version="6.0.0.0"/>
<application>
<windowsSettings>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
</assembly>
の内容を .manifest ファイルとして保存し(ここでは仮に ActiveCodePageUTF8.manifest)、
「プロジェクト」 → 「xxxx プロパティページ」 → 「構成プロパティ」 → 「マニフェストツール」 → 「入出力」 → 「追加のマニフェストファイル」
に、そのファイルパス名を追加します。
試しに Windows API、C標準ライブラリ、C++標準ライブラリで char 文字列パス指定の
CreateFileA(Win-API)、fopen(c標準)、ofstream(c++標準)
を用いる以下のサンプルを用意。
#include <windows.h>
#include <stdio.h>
#include <string>
#include <fstream>
#include <filesystem>
int main(int argc, char* argv[]) {
UINT sav = GetConsoleOutputCP();
SetConsoleOutputCP(65001);
for (int i = 1; i < argc; ++i) {
printf("引数 %d : %s\n", i, argv[i]);
std::string fname(argv[i]);
HANDLE h = CreateFileA((fname + "-winA(CreateFileA)").c_str(), FILE_GENERIC_WRITE
, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h != INVALID_HANDLE_VALUE) {
WriteFile(h, fname.c_str(), DWORD(fname.size()), NULL, NULL);
CloseHandle(h);
}
FILE* fp = fopen((fname + "-c(fopen)").c_str(), "wb");
if (fp) {
fwrite(fname.c_str(), 1, fname.size(), fp);
fclose(fp);
}
std::ofstream ost(fname + "-c++(ofstream)");
if (ost.is_open()) {
ost << fname;
}
}
for (auto const& file : std::filesystem::directory_iterator("."))
printf("%s\n", file.path().string().c_str());
// https://blog.techlab-xe.net/using-string-utf-8-winapi/ にある件の確認.
OutputDebugStringA("終わり\n"); // Win10 22h2, VS2022community で文字化けでした.
SetConsoleOutputCP(sav);
return 0;
}
Visual Studio でビルドして
> sample3.exe ねこ😺 ハート❤
のように実行すると
引数 1 : ねこ😺
引数 2 : ハート❤
.\ねこ😺-c(fopen)
.\ねこ😺-c++(ofstream)
.\ねこ😺-winA(CreateFileA)
.\ハート❤-c(fopen)
.\ハート❤-c++(ofstream)
.\ハート❤-winA(CreateFileA)
のように、SJIS に変換できない UNICODE 文字も無事で、期待通りの結果になりました。
ちなみに、コマンドラインでコンパイルする場合は
cl -std:c++20 -utf-8 -EHsc sample3.cpp
mt -manifest ActiveCodePageUTF8.manifest -outputresource:sample3.exe
のように mt コマンドで .manifest ファイルを exe に追記すればいいようです。
また cmake では、add_executable のソースの一つとして manifest ファイルを受け付けるようで、ActiveCodePageUTF8.manifest を追加すればすむようです。
4. おわりに
もともと std::filesystem::path とか tchar.h 利用とか UTF8⇔UTF16 変換ネタで一度書き終えていたのですが、3. のサイトを見てしまい思いっきり変更を余儀なくされたのでした。
いやホント知ることができてよかったです。
Windows10 1903 以前の Windows では Windows API の UTF8 化は使えませんが、現役で使われている PC の Windows10/11 ならば普通 OS 更新がされていると思われるので気にしないほうが幸せになれそうです。
※2023-09-10 追記
vc++ の std::filesystem は内部で UNICODE(UTF16) API が使われていて、c++ 標準関数での UTF8 API の確認には不適切だったので、sample3.cpp を ofstream を使う形に修正しました。
その他 cmake での manifest ファイルの件の追記とか諸々。
しかし std::filesystem::path が wchar_t ベースなのは、Win 以外が UTF8 なので "文字列"のLの有無とか面倒で、パス名操作で使うには結構つらい... path 自体は文字コード変換で重宝するのですが。
Discussion