explorer、お前もか(ワイド文字拡張\\?\の話)

2005年くらいまではあたし、本業が Windows C++ プログラマだったので、日常的にマイクロソフトの Win32 API ドキュメント等をみていて、「ワイド文字拡張\\?\」は 常識 だった。だってどの API 参照しても必ず書いてあるんだもん。

この関数の ANSI 版では、名前は最大 MAX_PATH 文字に制限されています。この制限をほぼ 32,000 ワイド文字へ拡張するには、この関数の Unicode 版を呼び出し、パスの前に “\\?\” という接頭辞を追加してください。詳細については、MSDN ライブラリの「ファイル名の規則」を参照してください。

ちなみに、MAX_PATH はほぼ 8bit の最大、32,000 は 16bit のまぁまぁ最大(符号付16bit整数の最大は32768)。

ネイティブ Windows C++ プログラマ以外はわからないであろう、でもネイティブ Windows C++ プログラマには常識のこと、もうひとつ。「Unicodeサポートのシカケ」の Windows API 流儀のこと。

  1. Windows 9x は Win32 API は全て ANSI (非 Unicode)
  2. Windows 9x 時代から、NT 系では Win32 API は ANSI 版/Unicode 版両方使えた
  3. Windows 9x 時代から、NT 系のシステム本体は全て Unicode 版
  4. Windows 9x 時代から、NT 系で ANSI 版 API を使うと、秘密裏に Unicode に変換されていた
  5. 全ての Windows が Unicode ビルドを利用出来るようになったのは Windows XP になって「9x が消滅」してから。
  6. この二重化のために、全ての「文字列を扱う Win32 API」は、ANSI/Unicode ビルドを「コンパイル時点で」切り替えられるマクロで提供されている

そしてこれ、もはや ANSI ビルドなんか必要ない、というほどに 9x ユーザなんかいなくなっているはずだけれども、いまだにこれはそのまま。

上画像で見えている「CreateFile」は、本当はこう:

WinBase.h
 1 WINBASEAPI
 2 __out
 3 HANDLE
 4 WINAPI
 5 CreateFileA(
 6     __in     LPCSTR lpFileName,
 7     __in     DWORD dwDesiredAccess,
 8     __in     DWORD dwShareMode,
 9     __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
10     __in     DWORD dwCreationDisposition,
11     __in     DWORD dwFlagsAndAttributes,
12     __in_opt HANDLE hTemplateFile
13     );
14 WINBASEAPI
15 __out
16 HANDLE
17 WINAPI
18 CreateFileW(
19     __in     LPCWSTR lpFileName,
20     __in     DWORD dwDesiredAccess,
21     __in     DWORD dwShareMode,
22     __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
23     __in     DWORD dwCreationDisposition,
24     __in     DWORD dwFlagsAndAttributes,
25     __in_opt HANDLE hTemplateFile
26     );
27 #ifdef UNICODE
28 #define CreateFile  CreateFileW
29 #else
30 #define CreateFile  CreateFileA
31 #endif // !UNICODE

これを、ビルド時点でどちらでビルドするかを「UNICODE」で決める(というより両方の構成が作られる)。

で、こんな話はまぁどうでも良くて、「一部の住人の常識」をひけらかすことが目的じゃないのでね。

この件で、どうしても以前から釈然としないことが2つあって。

ひとつ目。マイクロソフトが誠実に「しつこく」、「Unicode版ではワイド文字拡張で」の説明をしているにも関わらず、「explorerが」これに関係するバグを持っていた(る?)こと。つまり MAX_PATH 制約地雷を自分で踏んで自滅していたこと。深いフォルダの中の長いファイル名のファイルが消せなくなったり移動出来なくなるのはこれが原因。

もう一つが、なぜに開発者に対してはあれほどまでにしつこく説明するのに、エンドユーザに対して周知徹底しないのか、ということ。実はエンドユーザは普段からワイド文字拡張を使える:

ただし、エクスプローラは入力は受け付けてくれるが、意地でも c:\ に戻してしまうけれど。

なお、UNC の場合は「\\?\UNC\127.0.0.1\share」のようにします。

さて。ここからは Python の話。以上のことはワタシはずっとわかっていたことだけれども、Python でパスを扱うのに、イチイチこれを真面目にやろうとはしてこなかったのだけど、ちょっとやってみようかな、と思ってさ。ま、こんなだわ:

 1 >>> import os
 2 >>> os.listdir("c:\\Users")
 3 ['All Users', 'Default', 'Default User', 'desktop.ini', 'hhsprings', 'Public']
 4 >>> os.listdir("\\\\?\\c:\\Users")
 5 Traceback (most recent call last):
 6   File "<stdin>", line 1, in <module>
 7 WindowsError: [Error 123] ファイル名、ディレクトリ名、またはボリューム ラベルの構文が間違っています。: '\\\\?\\c:\\Users/*.*'
 8 >>> os.listdir(u"\\\\?\\c:\\Users")
 9 [u'All Users', u'Default', u'Default User', u'desktop.ini', u'hhsprings', u'Public']
10 >>> os.listdir("c:/Users")
11 ['All Users', 'Default', 'Default User', 'desktop.ini', 'hhsprings', 'Public']
12 >>> os.listdir("//?/c:/Users")
13 ['All Users', 'Default', 'Default User', 'desktop.ini', 'hhsprings', 'Public']

そもそも「\」で書くのはダルいので「/」で書こうぜ、はいいとして、

  1. 「”\\\\?\\c:\\Users”」で失敗するものが「u”\\\\?\\c:\\Users”」で成功する
  2. 「”//?/c:/Users”」でも成功し、「u”//?/c:/Users”」でも成功する

の対応関係が謎だ。