背景
- レイトレーシングとか, 画像処理とか, 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) orwchar_t
(UTF16)- Linux, clang/gcc などポータビリティ考慮するなら, UTF8 prefix 利用 + ソースコード UTF8 +
-DUNICODE
でビルドが推奨
- Linux, clang/gcc などポータビリティ考慮するなら, UTF8 prefix 利用 + ソースコード UTF8 +
- 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_filebuf
で std::ifstream
にバッファを渡すことできます.
llvm-mingw(libc++)では, _LIBCPP_HAS_OPEN_WITH_WCHAR
が定義されていれば(llvm-mingw では定義されていた), std::ifstream
へ wchar_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::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
に変換します.
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
読み込みがどうなるか調査する.