Windows でも main 関数の引数を UTF-8 に

Unicode が普及した現在では、macOS や Linux などでは UTF-8 を前提としても概ね問題ないでしょう。すると Windows の main も UTF-8 にしたくなります。


# main の引数を UTF-8 に設定する

main の引数を UTF-8 にするため、以下を設定します。

https://learn.microsoft.com/ja-jp/windows/apps/design/globalizing/use-utf8-code-page

ソース コードも UTF-8 が想定されるので、コンパイル オプション /utf-8 も設定しておきます。

https://learn.microsoft.com/ja-jp/cpp/build/reference/utf-8-set-source-and-executable-character-sets-to-utf-8


# コンソールのコードページ

https://learn.microsoft.com/ja-jp/windows/console/console-application-issues

日本語版 Windows の既定ではコードページが 932 なので

<dl>
<dd>入力： <code>SetConsoleCP(CP_UTF8)</code></dd>
<dd>出力： <code>SetConsoleOutputCP(CP_UTF8)</code></dd>
</dl>

を設定しないと文字化けします。

```c++:main_utf8.cpp
#include <stdio.h>

static int utf8len(unsigned char c)
{
    // if (c < 0x80) return 1;
    if (c < 0xc0) return 1;  // error
    if (c < 0xe0) return 2;
    if (c < 0xf0) return 3;
    if (c < 0xf8) return 4;
    return 1;  // error
}

static unsigned toucs(const unsigned char* p)
{
    switch (utf8len(p[0]))
    {
    default:
        return p[0];
    case 2:
        return (((p[0] & 0x1f) << 6) |
            ((p[1] & 0x3f) << 0));
    case 3:
        return (((p[0] & 0x0f) << 12) |
            ((p[1] & 0x3f) << 6) |
            ((p[2] & 0x3f) << 0));
    case 4:
        return (((p[0] & 0x07) << 18) |
            ((p[1] & 0x3f) << 12) |
            ((p[2] & 0x3f) << 6) |
            ((p[3] & 0x3f) << 0));
    }
}

static void print_argv_utf8(int i, const char* p)
{
    const unsigned char* q = (const unsigned char*)p;

    printf("argv[%d]: %s\n", i, p);
    while (*q)
    {
        size_t nc = utf8len(*q);
        unsigned ucs = toucs(q);

        printf("  char[%d] =", (int)((const char*)q - p));
        switch (nc)
        {
        case 1:
            printf("'%.1s' : %#04x\n", q, q[0]);
            break;
        case 2:
            printf("'%.2s' : %#04x, %#04x -> %#05x\n", q, q[0], q[1], ucs);
            break;
        case 3:
            printf("'%.3s' : %#04x, %#04x, %#04x -> %#06x\n", q, q[0], q[1], q[2], ucs);
            break;
        case 4:
            printf("'%.4s' : %#04x, %#04x, %#04x, %#04x -> %#08x\n", q, q[0], q[1], q[2], q[3], ucs);
            break;
        }
        q += nc;
    }
}

int main(int argc, char** argv)
{
    int i;

    printf("argc: %d\n", argc);
    for (i = 1; i < argc; i++)
        print_argv_utf8(i, argv[i]);
    return 0;
}
```

```console:引数「🈳の.txt」の実行結果
argc: 2
argv[1]: 圧縺ｮ.txt
  char[0] ='圧' : 0xf0, 0x9f, 0x88, 0xb3 -> 0x01f233
  char[4] ='縺ｮ' : 0xe3, 0x81, 0xae -> 0x306e
  char[7] ='.' : 0x2e
  char[8] ='t' : 0x74
  char[9] ='x' : 0x78
  char[10] ='t' : 0x74
```

見事に化けました。SetConsoleOutputCP(CP_UTF8) を使います。

```diff:main_utf8.cppとの差分
diff -u main_utf8.cpp main_utf8a.cpp
--- main_utf8.cpp	2024-05-23 20:15:44
+++ main_utf8a.cpp	2024-05-23 20:16:44
@@ -1,4 +1,5 @@
 #include <stdio.h>
+#include <Windows.h>
 
 static int utf8len(unsigned char c)
 {
@@ -65,6 +66,7 @@
 {
     int i;
 
+    SetConsoleOutputCP(CP_UTF8);
     printf("argc: %d\n", argc);
     for (i = 1; i < argc; i++)
         print_argv_utf8(i, argv[i]);
```

```console:引数「🈳の.txt」の実行結果
argc: 2
argv[1]: 🈳の.txt
  char[0] ='🈳' : 0xf0, 0x9f, 0x88, 0xb3 -> 0x01f233
  char[4] ='の' : 0xe3, 0x81, 0xae -> 0x306e
  char[7] ='.' : 0x2e
  char[8] ='t' : 0x74
  char[9] ='x' : 0x78
  char[10] ='t' : 0x74
```


#  Windows バージョン 1903 より前での実行

ドキュメントにあるように、上記の方法で作ったプログラムは Windows バージョン 1903 より前で実行すると UTF-8 ではなくなるので、古めの Windows で実行するとなると wmain を使用した方がいいようです。

https://learn.microsoft.com/ja-jp/cpp/c-language/using-wmain

wmain を使う場合、wmain の引数を wchar_t から UTF-8 に変換して main を呼び出す必要があります。すると、main 関数と wmain 関数がリンク時に両方存在することになるので、Visual Studio の既定動作では

```console:
LINK : warning LNK4067: エントリ ポイントがあいまいです。'mainCRTStartup' が選択されます。
```

となって、wmain が呼び出されません。wmain から開始するには、プロジェクトのプロパティで、エントリ ポイントを wmainCRTStartup にします。

![Visual Studio プロパティ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226677/7d3288d2-7655-022e-740c-9f2ac2170728.png)

このオプションを使わない方法として

```C++:
#define main _main_utf8
```

などとして main の名称を変更すると、コンパイル結果としての main 関数は存在しないので wmain から開始されます。

```c++:wmain.cpp
// ----------------------------------------------------------------------------
// Windows のみ  CRT と main の間に wmain が介在する形式になる
// ----------------------------------------------------------------------------
#ifdef _MSC_VER
#include <Windows.h>
#include <string>

// オプション wmainCRTStartup を不要にする措置.
#define main _main_utf8
int main(int argc, char** argv, char** envp);

// サロゲートペア第1文字?
static bool issurrogate1(wchar_t c)
{
    return (0xd800 <= c && c < 0xdc00);
}

// サロゲートペア第2文字?
static bool issurrogate2(wchar_t c)
{
    return (0xdc00 <= c && c < 0xe000);
}

/*
 * 変換に必要な情報を得る
 */

struct arginfo
{
    size_t size;   // 変換後のサイズ.
    size_t count;  // 文字列の数.
    wchar_t **pp;
};

static size_t utf8slen(const wchar_t* s)
{
    size_t r = 1;
    unsigned c;

    while ((c = *s++))
    {
        ++r;
        if (c < 0x80)
            continue;
        ++r;
        if (c < 0x800)
            continue;
        ++r;
        if (!issurrogate1(c))
            continue;
        if (!issurrogate2(*s))
            continue;
        ++s;
        ++r;
    }
    return r;
}

static arginfo argslen(wchar_t** pp)
{
    arginfo r = { sizeof(char*) + 1, 0, pp };
    while (pp[r.count])
        r.size += utf8slen(pp[r.count++]);
    r.size += r.count * sizeof(char*);
    return r;
}

/*
 * 文字列リストの変換
 */

static size_t cnvstr(char *d, wchar_t *s)
{
    unsigned c;
    wchar_t c1, c2;
    char *p = d;

    while ((c1 = *s++))
    {
        if (c1 < 0x80)
        {
            *p++ = char(c1);
            continue;
        }
        if (c1 < 0x800)
        {
            *p++ = char(0xc0 | ((c1 >> 6) /* & 0x1f */));
            *p++ = char(0x80 | ((c1 >> 0) & 0x3f));
            continue;
        }
        if (issurrogate1(c1))
        {
            c2 = *s;
            if (issurrogate2(c2))
            {
                ++s;

                c = 0x10000 + ((c1 & 0x03ff) << 10) + (c2 & 0x03ff);
                *p++ = char(0xf0 | ((c >> 18) /* & 0x07 */));
                *p++ = char(0x80 | ((c >> 12) & 0x3f));
                *p++ = char(0x80 | ((c >> +6) & 0x3f));
                *p++ = char(0x80 | ((c >> +0) & 0x3f));
                continue;
            }
         }

        *p++ = char(0xe0 | ((c1 >> 12) /* & 0x0f */));
        *p++ = char(0x80 | ((c1 >> +6) & 0x3f));
        *p++ = char(0x80 | ((c1 >> +0) & 0x3f));
        // continue;
    }
    *p++ = 0;
    return p - d;
}

static char **cnvarg(void *buffer, arginfo &ai)
{
    char **pp = (char**)buffer;
    char *p = (char*)(pp + ai.count + 1);
    for (size_t i = 0; i < ai.count; ++i)
        p += cnvstr((*pp++ = p), ai.pp[i]);
    *pp = NULL;
    *p = 0;
    return (char**)buffer;
}

/*
 * wmain
 */
int wmain(int argc, wchar_t** argv, wchar_t** envp)
{
    // コンソールの入出力を UTF-8 にする.
    SetConsoleCP(CP_UTF8);
    SetConsoleOutputCP(CP_UTF8);

    // スタック上に文字列リストを作成する.
    arginfo ai = argslen(argv);
    arginfo ei = argslen(envp);
    char **ap = cnvarg(_alloca(ai.size), ai);
    char **ep = cnvarg(_alloca(ei.size), ei);

    return main(argc, ap, ep);
}

#endif /* _MSC_VER */
// ----------------------------------------------------------------------------
// 以下、main 関数を含むプログラム
// ----------------------------------------------------------------------------
#include <stdio.h>

static int utf8len(unsigned char c)
{
    // if (c < 0x80) return 1;
    if (c < 0xc0) return 1;  // error
    if (c < 0xe0) return 2;
    if (c < 0xf0) return 3;
    if (c < 0xf8) return 4;
    return 1;  // error
}

static unsigned toucs(const unsigned char *p)
{
    switch (utf8len(p[0]))
    {
    default:
        return p[0];
    case 2:
        return (((p[0] & 0x1f) << 6) |
                ((p[1] & 0x3f) << 0));
    case 3:
        return (((p[0] & 0x0f) << 12) |
                ((p[1] & 0x3f) <<  6) |
                ((p[2] & 0x3f) <<  0));
    case 4:
        return (((p[0] & 0x07) << 18) |
                ((p[1] & 0x3f) << 12) |
                ((p[2] & 0x3f) <<  6) |
                ((p[3] & 0x3f) <<  0));
    }
}

static void print_argv(int i, const char* p)
{
    const unsigned char *q = (const unsigned char *)p;

    printf("argv[%d]: \"%s\"\n", i, p);
    while (*q)
    {
        size_t nc = utf8len(*q);
        unsigned ucs = toucs(q);

        printf("  char[%d] =", (int)((const char*)q - p));
        switch (nc)
        {
        case 1:
            printf("'%.1s' : %#04x\n", q, q[0]);
            break;
        case 2:
            printf("'%.2s' : %#04x, %#04x -> %#05x\n", q, q[0], q[1], ucs);
            break;
        case 3:
            printf("'%.3s' : %#04x, %#04x, %#04x -> %#06x\n", q, q[0], q[1], q[2], ucs);
            break;
        case 4:
            printf("'%.4s' : %#04x, %#04x, %#04x, %#04x -> %#08x\n", q, q[0], q[1], q[2], q[3], ucs);
            break;
        }
        q += nc;
    }
}

int main(int argc, char** argv, char** envp)
{
    int i;

    printf("wchar_t: %zd\n", sizeof(wchar_t));
    printf("argc: %d\n", argc);
    for (i = 1; i < argc; i++)
        print_argv(i, argv[i]);
    (void)envp;
    return 0;
}
```

```console:引数「🈳の.txt」の実行結果
argc: 2
argv[1]: "🈳の.txt"
  char[0] ='🈳' : 0xf0, 0x9f, 0x88, 0xb3 -> 0x01f233
  char[4] ='の' : 0xe3, 0x81, 0xae -> 0x306e
  char[7] ='.' : 0x2e
  char[8] ='t' : 0x74
  char[9] ='x' : 0x78
  char[10] ='t' : 0x74
```