7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C++11 ~ C++ 17 で Windows で fopen/fstream で UTF-8 or Windows パス名(UTF-16)を扱うメモ

Last updated at Posted at 2020-05-16

背景

  • レイトレーシングとか, 画像処理とか, compute intensive なアプリを書いている.
  • しかし画像ファイルとかの読み込みで Windows パス名(UTF-8?)に対応する必要がある
    • Linux や macOS などはあんまり問題がないようで, Windows のときだけ問題となる模様
  • アプリ内では, 特に UTF な文字列処理をしたいわけではない
    • 入力ファイル名や出力ファイル名がうまく扱えればよい

状況

fopen, std::fstream とかでファイルを読みかきするものとします.
locale は考慮しない(考慮したくない)とします.
文字列は, const char*, std::string or std::vector<char> で基本 8bit の配列として扱うものとして, コードが書かれているものとします.
wchar_t などなるべく使いたくないとします.

Windows

UNICODE

UCSとUTF
http://nomenclator.la.coocan.jp/unicode/ucs_utf.htm

C++

wchat_t はあんまり考えたくないものとします.
size は環境によってまちまちです. Windows では UTF-16(2 bytes)になります.

UTF-8 with C++ in a Portable Way
https://github.com/nemtrif/utfcpp

C++17: codecvt_utf8 is deprecated
https://codingtidbit.com/2020/02/09/c17-codecvt_utf8-is-deprecated/

とりあえずは, utfcpp 使えるなら使っておけば, 問題なさそうです.

Linux

Linux(posix + glibc?) では, UTF8(const char *)で特に問題はないようです.
普通に fopen, std::ifstream でいけるはずです.
(本当は wfopen, std::wifstream を使ったほうがいいかもだが)

Windows

Windows 10 or later, NTFS ファイルシステムを想定します.

通常, パス名は UTF-16(UTF-16LE?) で保存されていると想定できます.

What encoding are filenames in NTFS stored as?
https://stackoverflow.com/questions/2050973/what-encoding-are-filenames-in-ntfs-stored-as

Windowsの国際化について
https://qiita.com/false-git@github/items/16bc167a6bc995fbb715

Windows のコードページ (Windows10)とUnicodeの関係
https://qiita.com/zetamatta/items/1cb00c7313a9b38c7fc8

新規でプログラムを書くのであれば UNICODE で, 既存ライブラリで MBCS なものとリンクしないといけないのであれば, MBCS でビルドすることになるでしょうか

MSVC ランタイムの fopen では, そのままでは UTF-8 未対応ですが, fopen のオプションでエンコードを指定できました.

MinGW などで使えるかは不明ですので, wfopen を使うのが推奨でしょう.

MSVC の場合

ソースコードへの文字列定数の記述は,

const wchar_t *wfilename = L"./regression/日本語.exr";

のようにします. ただ, この場合, Windows は wchar_t は UTF-16 でしたので, UTF-16(LE) でソースコードファイルを保存しないとおかしくなります!(const char *filename = ... で UTF-8 で保存もダメ).

ただ, MinGW gcc, llvm-mingw(clang) では UTF16 なソースコードを読めません.

Windows(UTF-16LE で保存) or others(UTF-8 で保存)でファイル切り替えて include, もしくは, UTF8(UNICODE)限定であれば, 下記のように const char *s = u8"..." で指定という手が推奨でしょうか.
(C++11 から使えるようになった. 上記 L_TCHAR みたいな記述はレガシー用?)

まとめ

元が MBCS な入力(文字リテラル, コマンドライン引数の文字列)は考えないとします.

  • 入力や文字リテラルは const char *s = u8".."(UTF-8) or wchar_t(UTF16)
    • Linux, clang/gcc などポータビリティ考慮するなら, UTF8 prefix 利用 + ソースコード UTF8 + -DUNICODE でビルドが推奨
  • utf8 の場合, utf8 -> utf16(wchar_t) へ変換
  • wfopen, std::fstream などを wchart_t 文字列で呼び出す
  • 自作ライブラリなどで const char * な API interface を変えることができない場合は, wchar_t(UTF16) -> マルチバイト文字(MBCS)変換を行い, const char * へ変換する.
  • 自作ライブラリの API 実装の内部で, マルチバイト文字(const char * interface)から, wfopen など呼ぶようにする. MBCS -> Unicode(wchart_t)変換を行う(Win32 API に変換関数があるが, stack overflow などで見つかる変換コードを使ってもよいかもしれません)

入力の文字列は UTF-8 を想定するのがよいでしょう(e.g. ファイル名を, JSON など UTF-8 なテキストファイルから取得. Windows パス名が argv などで渡ってきたときは, cmd shell 側で UTF-8 に変換されているはず...?).

tinygltf のこのコミットあたりを参照ください.

C API ですと, multi-byte 文字列(const char * などで表現されている)を WideChar(wchar_t)に変換し, wfopen へ渡します.

fstream

MSVC だと, std::ifstream へ直接 std::wstring を渡すことができますが, これは MSVC 拡張になります.

MinGW(gcc) は glibc 拡張使わないとですが, wopen して, __gnu_cxx::stdio_filebufstd::ifstream にバッファを渡すことできます.

llvm-mingw(libc++)では, _LIBCPP_HAS_OPEN_WITH_WCHAR が定義されていれば(llvm-mingw では定義されていた), std::ifstreamwchar_t * でファイル名を渡すことができます.
(std::wstring は対応していなかった)

C++17 で std::filesystem が完全にサポートさている環境であれば,

How to open an std::fstream (ofstream or ifstream) with a unicode filename?
https://stackoverflow.com/questions/821873/how-to-open-an-stdfstream-ofstream-or-ifstream-with-a-unicode-filename

を参考にし,

std::ifstream ifs(std::filesystem::u8path(u8"こんにちは"));

とするのも手ですが, 残念ながら llvm-mingw(libc++)では現状(as of 18th May 2020) std::filesytem は未対応です.

また, wfstream は, ファイルの中身を wchar_t で扱うもののようなので, 入力ファイル名に std::wstringwchar_t * が使えるわけではありません.

MinGW, llvm-mingw

上記の通り, Windows ですと wchar_t は UTF-16 なので, 文字列定数を使う場合はソースコードも UTF16 で記述しないとうまくいきません.
しかし, 前述の通り, gcc, clang では UTF16 なソースを読むことができません. したがって UTF8 で記述し, プログラム内で wchar_t へ変換します.

文字列定数は, const char *s = u8"日本語" のように記述します.
(U or L prefix は MSVC 限定?)

ここから, char * を UTF8 文字列とみなし, 以下などを参考にし, wchar_t に変換します.

mingw gcc, llvm-mingw(clang) だと sizeof(wchar_t) == 2 になります.
(ちなみに Linux だと 4(UTF32) になります)

-DUNICODE -D_UNICODE で, UNICODE 設定でソースコードをコンパイルしましょう(Visual Studio での UNICODE 設定はこのマクロ定義を定義するだけっぽい) 

main 関数でコマンドライン引数から UNICODE(wchar_t) で文字列を取得する

-municode にして, wmain or _tmain で, UTF-8 設定ににしたコンソールから, wchar_t で文字列を受け取るのはできません(wWinMain が未定義エラーがでる. UNICODE 関連が未実装のため)

を参考にして __wgetmainargs 内部関数を呼んで引数取得になります(windows.hなどには expose されていないので, 自前で関数シグネチャを宣言しておく必要があります)

extern "C" int __wgetmainargs(int*, wchar_t***, wchar_t***, int, int*);

コンソール(cmd.exe, powershell)で, UTF-8 文字コードに設定しておきます. あとは,

> myprog.exe 日本語.exr

でうまく UTF8 文字列が wchar_t として渡るはずです!

おまけ: Windows resource file

.rc(リソースファイル)ですが, Visual Studio で生成すると UTF-16LE(with BOM)で生成されます. .rc に多言語文字が含まれていなければ, あとでテキストエディタで UTF-8 にしてもうまくいいとは思いますが, UTF-16 が推奨っぽいようです.

また, プロジェクトによっては, .gitattributes で強制的に utf-16le にしているものがあります. (e.g. embree3)

mingw gcc, llvm-mingw(clang, LLVM 10.0)では, UTF-16LE には対応していませんので, .rc をコンパイラに渡すとエラーになります.
(winres は OK っぽいよう?).

おまけ: clang では case-insensitive file path は扱えない

llvm-mingw(clang) + Ubuntu cross compile など, case sensitive なファイルシステムでは Windows.h が見つからないなどのエラーがでます.
リンク時のライブラリ名も同等です.

現状コンパイラ側で対応は進んでいないようです.

Add support for case-insensitive header lookup
https://reviews.llvm.org/D21113

したがって, ソースコードを書き換える(windows.h にする)くらいしか現状は対応策がありません.

Lowercase windows.h and uppercase Windows.h difference?
https://stackoverflow.com/questions/15466613/lowercase-windows-h-and-uppercase-windows-h-difference

とりあえず lower-case にしておけば, MSVC(NTFS)でも大丈夫そうです.

さらなるおまけ

Windows パスの文字列制限

Windows では, ファイルパスの長さに制限があります.

設定で緩和できるようです.

ありがとうございます.

拡張子のあとにスペースがある

git for Windows で, NTFS + 拡張子の最後にスペースがあると, git config core.protectNTFS true としている場合 git でうまく扱えないです.

TODO

  • 元の入力が MBCS の場合を考える.
  • llvm-mingw(clang) での UTF-8 の std::ifstream 読み込みがどうなるか調査する.
7
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?