C++11 ~ C++ 17 で Windows で fopen/fstream で UTF-8 or Windows パス名(UTF-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

https://stackoverflow.com/questions/3298569/difference-between-mbcs-and-utf-8-on-windows/3299860

## UNICODE

https://ja.wikipedia.org/wiki/Unicode

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

### C++

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

http://www.firstobject.com/wchar_t-string-on-linux-osx-windows.htm

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

https://docs.microsoft.com/en-us/cpp/atl-mfc-shared/unicode-and-multibyte-character-set-mbcs-support?view=vs-2019

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 のオプションでエンコードを指定できました.

https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/fopen-wfopen?view=vs-2019

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

https://github.com/syoyo/tinygltf/issues/221

https://github.com/OpenImageIO/oiio/issues/1349

### 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 のこのコミットあたりを参照ください.

https://github.com/syoyo/tinygltf/commit/a471770f38d7a821e869301168128834e6c906b5

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

##### fstream

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

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

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

https://github.com/ontio/libcxx-mirror/blob/master/include/fstream

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` は未対応です.

https://github.com/mstorsjo/llvm-mingw/issues/62

また, `wfstream` は, ファイルの中身を wchar_t で扱うもののようなので, 入力ファイル名に `std::wstring` や `wchar_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` に変換します.

https://stackoverflow.com/questions/148403/utf8-to-from-wide-char-conversion-in-stl/148665%23148665

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 関連が未実装のため)

https://gist.github.com/trueroad/fb4d0c3f67285bf66804

を参考にして `__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 では, ファイルパスの長さに制限があります.

https://techinfoofmicrosofttech.osscons.jp/index.php?%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%84%E3%83%91%E3%82%B9%E3%81%AE%E6%96%87%E5%AD%97%E5%88%97%E9%95%B7%E3%81%A8%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E5%95%8F%E9%A1%8C


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

https://ksk-log.com/post/2018/03/2018-03-17-longfilename/

ありがとうございます.

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

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

https://github.com/syoyo/tinygltf/issues/236#issuecomment-575502311

https://stackoverflow.com/questions/29294910/unable-to-add-files-with-name-containing-tilde-followed-by-a-number/29300360#29300360

## TODO

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