😺

Windows/VC++ で char を UTF8 で扱う

2022/07/10に公開

Windows/VC++ でも徐々に UTF-8 対応がしやすくなっていますが、過去との互換性のために SJIS(Shift-JIS)を引きずっており、他の OS や環境同様に char 型を UTF-8 で扱おうとすると少々面倒です。

オプションを設定し、コンソール出力のおまじないをし、さて、ファイルパスをどうしようかと、プログラムで対処……え、manifest を用意すればよい?!

というわけで、Windows 10/11 で、主に Visual Studio 2015-2022 の C/C++ で UTF-8 を使う場合のメモ書です。
※ Windows環境 や Windows API については古い VC や 他のコンパイラにも当てはまります。

1. ソーステキスト&バイナリでの char の UTF-8 対応

VC のデフォルトでは、ソーステキストの文字エンコードが Unicode か SJIS かに関わらず、char は SJIS(OS の文字エンコード)、wchar_t は UTF-16 でバイナリが作られるようになっています。

また、ソーステキストが UTF-8 の場合は BOM 付きでないと SJIS で解釈されて文字化けすることがあります。

VC 14.0(VS2015)以降で使えるコンパイラ・オプションとして

-utf-8                     ソース・テキスト&実行バイナリを UTF-8 で扱う.  
-source-charset:utf-8      ソース・テキストを UTF-8 で扱う.  
-execution-charset:utf-8   実行バイナリのcharを UTF-8 で扱う.  

がありますので、-utf-8 を指定しておけば、BOMなし UTF-8 のソースで問題なく、バイナリも UTF-8 で char 文字列が作られるようになります。

このオプション指定は、Visual Studio IDE 上の C++ プロジェクトのプロパティでは専用の設定項目がなさそうなので、

「プロジェクト」 → 「xxxx プロパティページ」 → 「構成プロパティ」 → 「C/C++」 → 「コマンドライン」 → 「追加のオプション」

に -utf-8 を追記します。

2. Windows での UTF-8 コンソール出力

2-1. 実行環境で対処

日本語 Windows のコマンドプロンプトは SJIS(Code Page 932)がデフォルトなので、UTF-8 でコンソール出力するプログラムは文字化けしてしまいます。

予めコマンドラインで、コードページを変更するコマンド chcp で

> chcp 65001

を実行して、UTF-8環境にすることで、文字化けせずに出力できるようになります。

UTF-8 専用のコマンドプロンプトのショートカットや Windows Terminal を設定する場合は、

%SystemRoot%\System32\cmd.exe /k chcp 65001

のように cmd.exe の引数として /k chcp 65001 を足して設定すればよいでしょう。

ただし、OS 付属のツールは UTF-8 対応していますが、SJIS(CP932)前提で作られたツールは当然文字化けしますので、混在して使う場合はどちらにしても注意が必要です。

2-2. プログラム側での UTF-8 コンソール出力

chcp は実行するプログラムのコンソール出力の文字エンコードを切り替えますが、コマンド・プロンプト(cmd.exe)や Windows Terminal の表示自体は、Unicode で行われています。

Windows API にはコンソール出力のコードページを切り替える API があり、プログラムが動いている間だけ UTF-8 出力にすることができます。

sample2-2.cpp
#include <stdio.h>
#include <windows.h>

int main() {
    UINT saveCP = GetConsoleOutputCP(); // コンソール出力の現在の Code Page を退避.
    SetConsoleOutputCP(65001);          // コンソール出力を UTF-8 に変更.

    printf("はろー❤わーるど\n");

    SetConsoleOutputCP(saveCP);         // コンソール出力の Code Page を元に戻す.
    return 0;
}

SetConsoleOutputCP で UTF-8 に切り替えるのですが、プログラムが終了しても設定がそのまま残るため、GetConsoleOutputCP で元のコードページを控えて、終了時に戻します。

これでコマンドプロンプトのコードページが SJIS,UTF-8 に関わらず、実行しても文字化けせずにすみます。

※ 既存の SJIS プログラムを手っ取り早く UTF-8 コンソール環境に対応したい場合、同様の手順で SetConsoleOutputCP(932) を用いるのも手です。

3. Windows API の UTF-8 対応

3-1. Windows 10 1903 以降

表示は前記で対処できますが、ファイルパスや main のコマンドライン引数(argv)等、OS とやり取りする char 文字列は SJIS(OS の文字コード)ままなので、UTF-8 文字列を用いる場合は対応が必要になります。

古い Windows では、UTF-8 文字列を wchar_t(UTF-16)文字列に変換して Windows API や MS拡張のライブラリ関数を用いて対応する必要がありましたが……

によると、Windows 10 1903 から ANSI系 Windows API で UTF-8 対応がされているようです。

もちろん実装内部で
 ANSI系 Windows API を使っている C標準ライブラリ や C++標準ライブラリ
対応になります。

3-2. manifest ファイル

対応にあたっては Active Code Page を UTF-8 にするための manifest ファイルを exe に埋め込みます。
Microsoft のページにかかれている

ActiveCodePageUTF8.manifest
<?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 とします。

VS IDE 上で設定する場合、

「プロジェクト」 → 「xxxx プロパティページ」 → 「構成プロパティ」 → 「マニフェストツール」 → 「入出力」 → 「追加のマニフェストファイル」

に、その manifest ファイルへのパス名を追加します。(コマンドラインについては後述)

3-3. 文字セット

VS IDE 上で 文字セット として

「プロジェクト」 → 「xxxx プロパティページ」 → 「構成プロパティ」 → 「詳細」 → 「文字セット」

  • 「設定なし」 (UNICODE _UNICODE _MBCS 未定義)
  • 「UNICODE 文字セット」 (UNICODE _UNICODE 定義 _MBCS 未定義)

を選択し、「マルチバイト文字セット」(_MBCS 定義)を避けます。

TCHAR ライブラリのマルチバイト文字対応は OS の文字コードで処理していて UTF-8 は不整合を起こしやすいですが、_MBCS 未定義時の1バイト文字の処理なら問題が起きにくいためです。

3-4. サンプル

試しに、CreateFileA(Win-API)、fopen(C標準ライブラリ)、ofstream(C++標準ライブラリ)を用いてみます。

sample3.cpp
#include <windows.h>
#include <stdio.h>
#include <string>
#include <fstream>
#if __cplusplus >= 201703L || _MSVC_LANG >= 201703L     // C++17以降.
#include <filesystem>
#endif

int main(int argc, char* argv[]) {
    UINT saveCP = GetConsoleOutputCP(); // コンソール出力の現在の Code Page を退避.
    SetConsoleOutputCP(65001);          // コンソール出力を UTF-8 に変更.

    for (int i = 1; i < argc; ++i) {
        printf("引数 %d : %s\n", i, argv[i]);
        std::string fname(argv[i]);

        // Windows API(ANSI) でファイル生成.
        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);
        }

        // C 標準ライブラリ でファイル生成.
        FILE* fp = fopen((fname + "-c(fopen)").c_str(), "wb");
        if (fp) {
            fwrite(fname.c_str(), 1, fname.size(), fp);
            fclose(fp);
        }

        // C++標準ライブラリでファイル生成.
        std::ofstream ost(fname + "-c++(ofstream)");
        if (ost.is_open()) {
            ost << fname;
        }
    }

  #if __cplusplus >= 201703L || _MSVC_LANG >= 201703L   // C++17以降.
    // カレントディレクトリのファイル名を一覧表示.
    for (auto const& file : std::filesystem::directory_iterator("."))
        printf("%s\n", file.path().string().c_str());
  #endif

    // https://blog.techlab-xe.net/using-string-utf-8-winapi/ にある件をついでに確認.
    OutputDebugStringA("終わり\n"); // Win10 22h2, VS2022community では文字化けでした.

    SetConsoleOutputCP(saveCP);     // コンソール出力の Code Page を元に戻す.
    return 0;
}

VS2017 以降で -std:c++17 を指定してビルドし、

> sample3.exe ねこ😺 ハート❤

を実行すると

引数 1 : ねこ😺
引数 2 : ハート❤
.\ねこ😺-c(fopen)
.\ねこ😺-c++(ofstream)
.\ねこ😺-winA(CreateFileA)
.\ハート❤-c(fopen)
.\ハート❤-c++(ofstream)
.\ハート❤-winA(CreateFileA)

のように、SJIS に変換できない UNICODE 文字も無事で、期待通りの結果になりました。

※ C++17 未対応の場合は一覧表示を省略します。その場合は別途 dir 等で確認してください。

3-5. コマンドラインでのビルド

コマンドラインにて VC でコンパイルする場合は

cl -std:c++17 -utf-8 -EHsc sample3.cpp
mt -manifest ActiveCodePageUTF8.manifest -outputresource:sample3.exe

のように mt コマンドで .manifest ファイルを exe に追記します。

※ mt のない他のコンパイラではもう一手間必要で、.rcファイル に RT_MANIFEST(24)で manifest ファイルを追記してリソース込みでビルドします。

3-6. CMake

CMake では、VC用の場合、add_executable のソースの一つとして manifest ファイルを受け付けるので、ActiveCodePageUTF8.manifest を追加するだけで済みます。

project(sample3)

add_executable(${PROJECT_NAME}
    "sample3.cpp"
    "ActiveCodePageUTF8.manifest"
)

※ "MinGW Makefiles" では .manifest は未対応ですが .rc は対応。"Watcom WMake" は .manifest .rc ともに未対応。

4. おわりに

もともと std::filesystem::path とか tchar.h 利用とか UTF-8⇔UTF-16 変換ネタで一度書き終えていたのですが、3. のサイトを見てしまい思いっきり変更を余儀なくされたのでした。
いやホント知ることができてよかったです。

Windows 10 1903 以前の Windows では Windows API の UTF-8 化は使えませんが、現役で使われている PC の Windows 10/11 ならば普通 OS 更新がされていると思われるので気にしないほうが幸せになれそうです。

※2022-07-10 初版
※2023-09-10 追記
VC++ の std::filesystem は内部で UTF-16 の UNICODE API が使われていて、C++ 標準関数での UTF-8 API の確認には不適切だったので、sample3.cpp を ofstream を使う形に修正しました。
その他 cmake での manifest ファイルの件の追記とか諸々。

しかし std::filesystem::path が wchar_t ベースなのは、Win 以外が UTF-8 なので "文字列"のLの有無とか面倒で、パス名操作で使うには結構つらい... path 自体は文字コード変換で重宝するのですが。

※2024-11-22 追記
文字セットについて等追加、その他諸々修正。

Discussion