WindowsのC言語でUnicodeのファイル名をfopenする: fopen unicode file name

328, 2021-09-22

目次

結論から言うと

結論から言うと、Windows環境のC言語(GCC)のfopen()関数は、ソースコード内のUnicodeなファイル名を開くことができません。
日本語などのUnicodeが混じったファイル名をfopen()に渡すとfopen()はファイルを開くのに失敗します。

そのためWindows環境のC言語でUnicodeなファイル名を開くには次のようにする必要があります。
マルチバイト列なファイル名をいったんワイド文字列に変換します。
そして_wfopen()というワイド文字列に対応したファイルオープン関数で開きます。

この記事ではこれらの具体的な方法を解説します。

Unicodeのファイル名をfopen()で開けないのはなぜか?

なぜWindowsではUnicodeのファイル名をオープンできないのでしょうか?
具体的な理由は不明です。
検証してみると、システムコールのopen()もUnicodeのファイル名を開けないようです。
fopen()は↓のようにソースコード内に日本語のファイル名がベタ書きされていた場合、ファイルを開くのに失敗します。

#include <stdio.h>

int main(void) {
    FILE *fin = fopen("日本語.txt", "r");
    if (!fin) {
        perror("fopen");
        return 1;
    }

    printf("fin[%p]\n", fin);
    fclose(fin);
    return 0;
}

「日本語.txt」はプログラムと同じ階層に存在するファイルです。
↑のコードをWindows環境のgccでコンパイルして実行すると、↓のような結果になります。

fopen: No such file or directory

↑のようにfinNULLになり、perror()が実行されます。

Windowsのfopen()はベタ書きされたUnicodeのファイル名を開けない

Windows環境のgccなどのfopen()はUnicodeなファイル名を開けないということになります。
つまり、プログラムの中で日本語などが含まれるファイル名のファイルを開くことができません。

C言語は古くからある言語なので、コンパイラによってはUnicodeな文字列の対応がいまいちなところがあります。
これはC言語を拡張したC++でも同じことが言えます。
そのためコンパイラによっては開発者の方でテコ入れをしてあげる必要があるようです。

ただしLinux環境のgccでは特に日本語のファイル名などは問題ないようです。
つまりWindows固有の問題と言えます。

Windowsのfopen()でUnicodeのファイル名を開くには?

Windows環境のgccなどで、ソースコードにベタ書きされたUnicodeなファイル名を開くには↓のようにする必要があります。

  • Unicodeなファイル名をワイド文字列に変換する

  • ワイド文字列なファイル名を_wfopen()で開く

ファイル名をワイド文字列に変換し、_wfopen()で開く

const char *char[]などで定義されたマルチバイト文字列なUnicodeのファイル名は、そのままだと_wfopen()に渡せません。
そのためいったんこのマルチバイト文字列なファイル名をワイド文字列に変換します。
そしてワイド文字列に対応した_wfopen()というWindows固有のAPIでファイルを開きます。

_wfopen()はWindows固有のAPIです。
そのためLinux環境のgccなどでは利用できません。
つまり移植性を考慮して、マルチプラットフォームなコードを書く場合はこれらのAPIの使用を考慮する必要があります。

ファイル名のワイド文字列への変換方法

マルチバイト列なUnicodeのファイル名をワイド文字列に変換するにはどうしたらいいでしょうか?
そのために使う関数は?
具体的に見ていきます。

MultiByteToWideChar()を使う

MultiByteToWideChar()関数はwindows.hをインクルードすると使える関数です。
これはWindows固有のAPIで、マルチバイト文字列をワイド文字列に変換します。

この関数を使えばUnicodeなマルチバイト文字列をワイド文字列に変換できます。
具体的には↓のようなコードを書きます。

#include <windows.h>
#include <stdio.h>
#include <locale.h>

int main(void) {
    setlocale(LC_ALL, "");  // ロケールの設定

    const char *s = "日本語";  // マルチバイト文字列

    // sのワイド文字列として長さを得る
    size_t buflen = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);

    // ワイド文字列のためのバッファを確保する
    wchar_t *ws = calloc(buflen + 1, sizeof(wchar_t));

    // sをワイド文字列に変換してバッファに保存する
    if (MultiByteToWideChar(CP_UTF8, 0, s, -1, ws, buflen) == 0) {
        free(ws);
        return 1;
    }

    // ワイド文字列の出力
    wprintf(L"[%s]\n", ws);

    free(ws);
    return 0;
}

↑のコードは「日本語」というマルチバイト文字列をワイド文字列に変換するコードです。
変換には2回MultiByteToWideChar()を使っています。

1回目のMultiByteToWideChar()で変数sのワイド文字列としての長さを得ます。
この時点ではバッファへの保存は行いません。

そのあとに文字列の長さだけ動的メモリの確保でバッファを確保します。
そして2回目のMultiByteToWideChar()の呼び出しで、sをワイド文字列に変換してバッファに保存します。

変換したワイド文字列wswprintf()で出力します。
出力はロケールを設定しないと端末には正しく表示されません。

注意事項として、ソースコードのエンコーディングはUTF-8にしておく必要があります。

ワイド文字列のファイル名を_wfopen()で開く方法

MultiByteToWideChar()で変換したファイル名を_wfopen()に渡せばファイルを開くことができます。
ワイド文字列への変換も含めたコードは↓になります。

#include <windows.h>
#include <stdio.h>

int main(void) {
    const char *path = "日本語.txt";  // マルチバイト文字列なファイル名

    // マルチバイト文字列をワイド文字列に変換する
    size_t buflen = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
    wchar_t *wpath = calloc(buflen + 1, sizeof(wchar_t));
    if (MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, buflen) == 0) {
        free(wpath);
        return NULL;
    }

    // ファイルを読み込みモードで開く
    FILE *fp = _wfopen(wpath, L"r");
    if (!fp) {
        perror("_wfopen");
        return 1;
    }

    printf("fp[%p]\n", fp);  // ファイルポインタの値を出力
    fclose(fp);  // ファイルをクローズ
    return 0;
}

↑のソースコードをコンパイルします。
そしてプログラムと同じ階層に「日本語.txt」というファイルを置きます。
プログラムを実行すると「日本語.txt」を開くことに成功します。

_wfopen()はLinuxでは使えない

注意事項として、_wfopen()はWindows固有のAPIです。
そのためLinuxのGCCなどでは使うことができません。
そのため移植性を考量する場合は、これらの関数をラップする必要があります。

WindowsとLinuxの両方に対応したラッパー関数を作る

ではWindowsとLinuxの両方に対応したラッパー関数を作ってみましょう。
関数名はopen_file()とします。
open_file()の実装は↓のようになります。

#if defined(_WIN32) || defined(_WIN64)
# define IS_WIN 1
#endif

#ifdef IS_WIN
# include <windows.h>
#endif

#include <stdio.h>

FILE *
open_file(const char *path, const char *mode) {
    if (!path || !mode) {
        return NULL;
    }

#ifdef IS_WIN
    // Windowsではここのコードがコンパイルされる
    // pathをワイド文字列に変換する
    size_t buflen = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
    wchar_t *wpath = calloc(buflen + 1, sizeof(wchar_t));
    if (MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, buflen) == 0) {
        free(wpath);
        return NULL;
    }

    // modeをワイド文字列に変換する
    buflen = MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0);
    wchar_t *wmode = calloc(buflen + 1, sizeof(wchar_t));
    if (MultiByteToWideChar(CP_UTF8, 0, mode, -1, wmode, buflen) == 0) {
        free(wpath);
        free(wmode);
        return NULL;
    }

    // ファイルを開く
    FILE *fp = _wfopen(wpath, wmode);
    free(wpath);
    free(wmode);
#else
    // Windows以外ではここのコードがコンパイルされる
    FILE *fp = fopen(path, mode);
#endif
    return fp;
}

int main(void) {
    FILE *fin = open_file("日本語.txt", "r");
    if (!fin) {
        perror("open_file");
        return 1;
    }

    printf("fin[%p]\n", fin);
    fclose(fin);
    return 1;
}

↑のコードはWindows環境、Linux環境のGCCでコンパイル可能です。
また日本語のファイルもちゃんと開くことができます。

_WIN32_WIN64はWindows環境で定義されるマクロ定数です。
この定数をチェックし、存在していたらIS_WINマクロ定数を定義します。
そしてIS_WINマクロ定数が定義されていたらwindows.hをインクルードします。
さらにopen_file()内で処理を分岐します。

このように実装することでマルチプラットフォームなファイルオープン処理が可能になります。

おわりに

今回はWindows環境のC言語でUnicodeなファイル名をfopen()する方法を解説しました。
Unicodeなファイル名を開けないというのはC言語の歴史を感じさせますが、以上のように対応すればファイルを開くことは可能です。

ちなみにWindows環境のstat()なども日本語のファイル名は開けません。
これについては別記事にしようと思います。

C言語の歴史を感じよ

時代はUnicode