タイトルの件である。(実際にはTeXworksは問題の核心に1ミリも関係しないので、TeXworksを使っていないあなたもこの記事を読み物として楽しむことができる。さあ読もう)
この記事を書いている6月現在、TeX Live Managerで最新版にアップデートすれば問題は解決するはずである。
この記事では、筆者がどのようにこの問題の原因を突き止めたか、順に書き連ねていく。なお、問題の発覚と原因究明は5月初めに行ったが、TeX Liveのアップデートで対策されるようになるのを待った結果記事の公開が6月になった(筆者がよくブログ記事を下書きのままほったらかしているのとは関係ない)。
目次
遭遇
Twitterで次のような事象を見かけた:
うん、そうだよね(TeX関係/パズル/ぬりかべテスト.tex) pic.twitter.com/V8XYJUDnkY
— ぷりにゃん (@puripuri2100) May 6, 2018
google drive file streamに保存したTeXファイルをダブルクリックで開こうとするとファイル名が文字化けして指定されたパスが見つかりませんと言われる…… pic.twitter.com/Jbhr11doVn
— えとな (@PrimordialOcean) May 6, 2018
TeXworksを立ち上げておいてからファイル→開くとしたりメモ帳から開く分には問題ないみたい。
— えとな (@PrimordialOcean) May 6, 2018
デスクトップにレポートのテンプレtexファイルを置いてるんだけど、texworksを起動したうえでそれにドラッグドロップで開くことはできるのに、直接開こうとするとパスがなくて場所が利用できない云々をいわれるのもそれかな
— Nesshie (@ashino38) May 7, 2018
どうやら、TeX Live 2018のTeXworksに問題があり、Shift_JISのファイル名を誤ってLatin-1として解釈しているようである。TeXworksのGUIでファイルを開くことに関しては問題はなく、コマンドライン引数の扱いに問題が発生していると考えられる。
この問題は筆者のWindows環境(Windows 10 バージョン1803)でも再現した:
同じWindows環境にTeX Live 2017が入っていたのでそちらでも試したが、TeX Live 2017のTeXworksではこの問題は起きなかった。
(Windows 10のApril 2018 Updateの適用後にこの事象が起こるようになった、という報告があるが、筆者はApril 2018 Update適用前の状態での動作を検証していない)
筆者のメイン機はMacで、TeXは普段Emacsで書いているのでこの問題で筆者自身が困ることはないのだが、 筆者の高い問題解決能力を世間に誇示するために 困っている人がいるのに見て見ぬふりはできないので首を突っ込むことにした。
TeX Live 2018で入るTeXworksがコマンドライン引数をLatin1で解釈するバグがあるようなので調査してみるか(研究しろ)
— 新オフトゥン生活 (@mod_poppo) May 7, 2018
TeXworksの調査
まずはTeXworksのソースコードを調査する。
最初にGitHub上のコードを眺めてみたが、main関数の引数 (argc, argv) はそのままQtに丸投げしており、TeXworks側のミスで文字化けが起こるということはなさそうである。
もしかするとGitHub上にある最新(開発版)のコードは修正済みかもしれないので、GitHubの最新の(開発版の)バージョンで問題が再現するか調査するべきである。
筆者のWindows上での開発にはMSYS2を使っているので、TeXworksもMSYS2上のMinGW-w64でビルドする。
公式のビルドマニュアルを参考にしつつ、pacmanで依存関係を適当に入れる(qt5, poppler, hunspell等)。CMakeでMakefileを作り、ビルドする。
$ git clone ほにゃらら/texworks $ cd texworks $ mkdir build; cd build $ cmake -G"MSYS Makefiles" -DQt5QWindows_LIBRARIES=C:/msys64/mingw64/share/qt5/plugins/platforms/qwindows.dll -DWITH_LUA=OFF -DTEXWORKS_ADDITIONAL_LIBS=shlwapi .. $ make -j8
そうやって出来上がったTeXworks.exeに日本語を含むファイル名を与えると、正常に開くことができた。つまりGitで開発中の最新バージョンには問題はない。
まとめると
- TeX Live 2017に含まれるTeXworksのバージョン:問題なし
- TeX Live 2018に含まれるTeXworksのバージョン:問題あり
- Gitで開発中のバージョン:問題なし
なので、この問題は開発中のTeXworksが正式リリースとなれば(新しいTeXworksがリリースされるのを待てば)自動的に解消する…と思われたが、話はそう簡単ではなかった。
TeXworksの最近のリリースを調べてみたところ、直近の正式版リリースは一年前(2017年4月)であり、TeX Live 2017のTeXworksとTeX Live 2018のTeXworksは同一バージョンだったのだ。コワイ!
なので、TeXworks本体ではなく、TeX Liveに収録される設定ファイルの類が変化したために問題が起きるようになった、と考える必要がある。
Runscript
さて、TeX Liveに収録されているTeXworksのバイナリ(bin/win32/texworks.exe)だが、妙にファイルサイズが小さい。どうもこれはコマンドライン引数を受け取ってTeXworksの本体(tlpkg/texworks/以下にある)に中継するだけのプログラムのようである。
この「コマンドライン引数を中継するプログラム」の背景について解説しておこう。
Unixの世界では、スクリプト言語で書かれたプログラムにshebangと実行可能属性をつけてコマンドとして使えるようにする、ということがよく行われている。
例えば、latexmkはPerlで書かれているし、texdocはLuaで書かれている。Unixではこのようなスクリプト言語で書かれたプログラムを、マシン語で書かれたバイナリファイルと同じように実行できる。
しかし、Windowsにはそのような機構はない(一応、Windowsにもバッチファイルだとか、PATHEXTで指定された拡張子のプログラムを実行できる機構はあるが、Unixのものとは性質が異なる)。Windowsでの実行可能コマンドと言ったら、あくまで.exeファイルなのである。
このほか、Unixではシンボリックリンクが多用されるが、Windowsでは歴史的事情からかシンボリックリンクは多用されない。ショートカットというものはあるが、ショートカットファイルそのものは(拡張子を省略して)実行することはできない。
そこでWindows向けのTeX Liveでは、Unix向けに書かれた各種スクリプトを呼び出すだけの簡単な.exeファイルを用意して、これらの問題に対処している。
bin/win32/以下にあるtexworks.exeもそういう「簡単な.exeファイル」の一つである。しかし bin/win32/texworks.exe 自身がTeXworksの本体を探して実行しているわけではない。bin/win32/texworks.exeは同じディレクトリにある runscript.dll に処理を丸投げしている。
runscript.dll では、LuaTeXのインタープリターを利用して runscript.tlu (見慣れない拡張子だが、中身はLuaスクリプト)を実行している。これによって、いちいちバイナリを再コンパイルしなくてもrunscript機構を調整できる。
runscript.tluではLuaTeXが提供するos.spawn関数を利用して外部コマンドを起動している。(なお、呼び出したいプログラムの本体がLuaで書かれている場合(texdocなど)は、外部コマンドとしてではなく、起動中のLuaTeXでそのまま実行する)
LuaTeXのos.spawnはCランタイムの_spawnvp関数を使ってコマンドを起動する。
Cランタイムの_spawnvp関数は、最終的にはWin32 APIであるCreateProcessAかCreateProcessWを使ってコマンドを起動しているはずである。
なお、Windowsのコマンドライン文字列・ファイル名・環境変数・コンソール入出力の類は内部的にはUTF-16なので、CreateProcessAも内部的には文字列をUnicodeに変換したうえでCreateProcessW相当の関数を呼び出しているはずである。(Unix系OSがこれらの文字列を単なるバイト列として扱うのとは対照的である)
誰が悪いのか
というわけで、Windows上のTeX Liveに収録されたTeXworksが起動するまで、以下のプログラム・関数が間に入ってくる:
- ラッパーの texworks.exe の WinMain 関数 (wrunscript_exe.c)
- runscript.dll の dllwrunscript, dllrunscript 関数 (runscript_dll.c)
- LuaTeX (luatex.dll) の dllluatexmain 関数 (luatex.c)
- runscript.tlu
- LuaTeX の os.spawn こと os_spawn 関数 (loslibext.c)
- Cランタイムの _spawnvp 関数
- (TeXworks 本体)
この一連の流れのどこかで、コマンドライン文字列が化けている。
しかし、runscript.dllのコード, LuaTeXのmain関数付近(コマンドライン文字列の扱い)、LuaTeXのos.spawn関数を眺めても特に不審な点は見当たらない。LuaTeXは比較的変化の激しい代物であるが、この一年で文字エンコーディングに関して変わったとは思えない。
というわけで消去法で、「Cランタイムの動作が変わった」という仮説が残る。
Cランタイム
TeX Live 2017とTeX Live 2018のbin/win32ディレクトリの中身を比較すると、後者に api-ms-win-ナントカ-カントカ.dll というファイルが増えているのがわかる。これはマイクロソフトがVisual Studio 2015で導入したUniversal CRTと呼ばれるものである。
LuaTeXが利用するCランタイムは、TeX Live 2017の時点では従来のMSVCRT(MSVCR100.DLL)だったのが、TeX Live 2018ではUniversal CRTに変わっている。
Universal CRTでは割と非互換な変更をガシガシ入れているようなので、その影響を受けてCランタイムの関数(ここで問題になるのは _spawnvp など、 system() 系の外部プロセス起動関数)の動作が変わったとしてもおかしくない。
system系の関数にはマルチバイト文字列を受け取るもの(system, _spawnvp等)とワイド文字列(UTF-16)を受け取るもの(_wsystem, _wspawnvp等)がある。前者の関数は、(日本語Windowsでは)引数を Shift_JIS (CP932) として解釈する…はずだった。
しかし「日本語 Windows ではマルチバイト文字列は Shift_JIS」というのは果たして当たり前なのだろうか。systemや_spawnvpはOSが提供する関数ではなく、Cランタイムの関数である。OSの設定でマルチバイト文字列がShift_JISであっても、Cランタイムの関数がそれに従う道理はない。実際、Cランタイムの関数(mbstowcs等)を使ってマルチバイト文字列を変換する際には、LC_CTYPEという変数が利用される。LC_CTYPEは、Cランタイムのsetlocale関数で取得・設定できる。
LC_CTYPEはプログラムの起動時には “C” に設定される。LuaTeXでも特にこの設定をいじるということはなく、runscript.tlu が os.spawn を呼び、Cランタイムの _spawnvp 関数が呼び出される時点でも LC_CTYPE は “C” のはずである。
マイクロソフトの提供するCランタイム(旧来のMSVCRTおよびUniversal CRT)では、LC_CTYPE=Cの場合、マルチバイト文字列をワイド文字列に変換する際には単に8ビット値を16ビット値に「拡張」する。Unicodeの8ビットで表される部分がLatin-1と同一であることを考えれば、これはマルチバイト文字列をLatin-1として扱っていることと同一である。
(関連記事:C言語のワイド文字入出力 — MSVCRTの場合)
というわけで、「従来のMSVCRTでは_spawnvp等の関数はLC_CTYPEの影響を受けなかったが、Universal CRTでは_spawnvp等の関数がLC_CTYPEの影響を受けるようになった」と考えれば、今回のTeX Live 2018/TeXworksの事象と完全に合致する。(ここが「Universal CRTでは」なのか「April 2018 Update以後のUniversal CRT」なのかは検証の余地がある)
検証
仮説を検証しよう。system関数とコマンドライン引数に関する、簡単なCプログラムを書く。
まず、コマンドライン文字列をそのまま出力するプログラム(dumparg)を書く。Windowsではコマンドライン文字列は内部的にはUnicode (UTF-16)なので、余計な変換を起こさないためにargvはワイド文字列で受け取る。マルチバイト文字列の変換が起こらない以上、このプログラムはCランタイムの仕様変更に関する影響は受けないはずである。
dumparg.cはどのバージョンのVisual C++でコンパイルしても良い(何ならMinGWでもよいが、その場合は -municode
のようなオプションが必要になるかもしれない)。
C:\...> cl dumparg.c Microsoft(R) C/C++ Optimizing Compiler Version 19.13.26129 for x64 Copyright (C) Microsoft Corporation. All rights reserved. dumparg.c Microsoft (R) Incremental Linker Version 14.13.26129.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:dumparg.exe dumparg.obj
次に、ロケールのLC_CTYPEを変更しつつ、さっき書いたプログラムを起動するプログラム (localetest.c) を書く。このプログラム中で呼んでいる_setmbcp/_getmbcpという関数は、MSDNで「OSとやりとりするCランタイム関数のマルチバイト文字コードに影響する」とされている関数である。
system関数に渡している\xb2\xb3, \xe3\x82\x81というバイト列は、各文字コードによってそれぞれ
- Latin-1 (CP1252): “\xb2\xb3″=”²³” (U+00B2 U+00B3), “\xe3\x82\x81″=”ゃ” (U+00E3 U+0082 U+0081)
- CP1253: “\xb2\xb3″=”²³” (U+00B2 U+00B3), “\xe3\x82\x81″=”γ‚” (U+03B3 U+0081 U+201A)
- Shift_JIS (CP932): “\xb2\xb3″=”イウ” (U+FF72 U+FF73), “\xe3\x81″=”縺” (U+7E3A)
- UTF-8: “\xe3\x82\x81″=”あ” (U+3042)
と解釈される。
まず、旧来のMSVCRTを使うVisual C++ 2013で試してみよう。
Visual C++ 2013(旧来のMSVCRT)での実行結果
具体的な出力はリンク先を見てほしいが、まとめると、
- 初期状態ではLC_CTYPE=C, mbcp=932で、system関数は与えられた文字列をCP932 (Shift_JIS)で解釈する。
- setlocale(LC_CTYPE, “”)を呼ぶと、LC_CTYPEは日本語/CP932 (Japanese_Japan.932)に変わる。system関数はやはり、与えられた文字列をCP932 (Shift_JIS)で解釈する。
- setlocale(LC_CTYPE, “.UTF-8”)は失敗する。LC_CTYPEもmbcpも元のままで、system関数に送った”\xe3\x81\x82″は無理やりShift_JISと解釈され、「縺」(Shift_JISでE381)に化けている。(この漢字、文字化けしたときによく見るやつだ!)
- setlocale(LC_CTYPE, “French_France”)を呼ぶと、LC_CTYPEはフランス語/CP1252 (French_France.1252)に変わる。mbcpは932のままで、system関数は、与えられた文字列をCP932 (Shift_JIS)で解釈する。
- setlocale(LC_CTYPE, “Greek_Greece”)を呼ぶと、LC_CTYPEはギリシャ語/CP1253 (Greek_Greece.1253)に変わる。mbcpは932のままで、system関数は、与えられた文字列をCP932 (Shift_JIS)で解釈する。
- _setmbcpを呼んでもsystem関数の挙動が変わるようには見えない。謎だ。
となる。つまり、system関数はマルチバイト文字列を常に(システムの文字コードである)Shift_JISで解釈しようとする。
次に、Universal CRTを使うVisual C++ 2017で試してみよう。
Visual C++ 2017(Universal CRT)での実行結果
具体的な出力はリンク先を見てほしいが、まとめると、
- 初期状態ではLC_CTYPE=C, mbcp=932で、system関数は与えられた文字列をCP932 (Shift_JIS)で解釈する。
- setlocale(LC_CTYPE, “”)を呼ぶと、LC_CTYPEは日本語/CP932 (Japanese_Japan.932)に変わる。system関数はやはり、与えられた文字列をCP932 (Shift_JIS)で解釈する。
- setlocale(LC_CTYPE, “.UTF-8”)は失敗する。LC_CTYPEもmbcpも元のままで、system関数に送った”\xe3\x81\x82″は無理やりShift_JISと解釈され、「縺」(Shift_JISでE381)に化けている。(この漢字、文字化けしたときによく見るやつだ!)
- setlocale(LC_CTYPE, “French_France”)を呼ぶと、LC_CTYPEはフランス語/CP1252 (French_France.1252)に変わる。mbcpは932のままで、system関数は、与えられた文字列をCP932 (Shift_JIS)で解釈する。
- setlocale(LC_CTYPE, “Greek_Greece”)を呼ぶと、LC_CTYPEはギリシャ語/CP1253 (Greek_Greece.1253)に変わる。mbcpは932のままで、system関数は、与えられた文字列をCP932 (Shift_JIS)で解釈する。
- _setmbcpを呼んでもsystem関数の挙動が変わるようには見えない。謎だ。
となる。つまり、system関数はマルチバイト文字列を常に(システムの文字コードである)Shift_JISで解釈しようとする。
これらの結果を見ると、予想に反して、旧来のMSVCRTとUniversal CRTでは動作は変わっていないように見える。
だが結論を出すのは早い。Cランタイムには静的リンク版と動的リンク版があり、デフォルトで使われるのは静的リンク版、LuaTeX等で使われていたのは動的リンク版だ。静的リンク版と動的リンク版で挙動が違うことは考えたくないが、 Microsoftのやることは信用できないので Microsoftの崇高な考えは下々の民には計り知れないので、あらゆる可能性を想定しなくてはならない。
(ちなみに、動的リンク版の挙動が変わったのであれば、「WindowsのアップデートによってTeXworksの挙動が変わった」という報告ともつじつまが合う。動的リンク版のUniversal CRTはWindowsのアップデートによって差し替えられるため。)
Cランタイムを動的リンクするには、 cl に /MD オプションを渡す。やってみよう。
Visual C++ 2017(Universal CRT・動的リンク)での実行結果
具体的な出力はリンク先を見てほしいが、まとめると、
- 初期状態ではLC_CTYPE=C, mbcp=932で、system関数は与えられた文字列をLatin-1で解釈する。
- setlocale(LC_CTYPE, “”)を呼ぶと、LC_CTYPEは日本語/CP932 (Japanese_Japan.932)に変わる。system関数は、与えられた文字列をCP932 (Shift_JIS)で解釈する。
- setlocale(LC_CTYPE, “.UTF-8”)は成功し、LC_CTYPEはJapanese_Japan.utf8に変わる。system関数は与えられた文字列をUTF-8で解釈する。
- setlocale(LC_CTYPE, “French_France”)を呼ぶと、LC_CTYPEはフランス語/CP1252 (French_France.1252)に変わる。system関数は与えられた文字列をLatin-1で解釈する。
- setlocale(LC_CTYPE, “Greek_Greece”)を呼ぶと、LC_CTYPEはギリシャ語/CP1253 (Greek_Greece.1253)に変わる。system関数は与えられた文字列をCP1253で解釈する。
- _setmbcpを呼んでもsystem関数の挙動が変わるようには見えない。謎だ。
となる。つまり、system関数は文字列をLC_CTYPEに従って解釈しようとする。先のセクションの最後での予想は正しかった。
対策
os.spawnの呼び出し時点でLC_CTYPEを”C”ではなく、システム設定に基づいたもの(日本語WindowsならCP932)にしておけば良い。
もちろん、”CP932″をハードコードしてしまうとそのプログラムは可搬性のないものになり、いろんな言語環境の人が使うTeX Liveに取り入れてもらうことはできなくなる。代わりに、setlocale関数の第2引数に空文字列 “” を指定すれば、システム設定を元にロケールを適当に設定してくれることになっている(Windowsの場合はレジストリかどこか、Unixの場合は環境変数など)ので、そうすればよい。
ではどこでsetlocale関数を呼ぶか?LuaTeXやrunscriptのC言語部分をいじるという手もあるが、書き換えが容易なLuaで書かれたrunscript.tluを書き換えるのが手だろう。C言語のsetlocale関数はLuaからはos.setlocaleという名前で呼べる(引数の順番に注意)。
というわけで、runscript.tluの先頭に
os.setlocale("", "ctype")
を書き加えたところ、筆者のWindows環境のTeX Live 2018でもTeXworksで日本語を含むファイル名を扱えるようになった。(注:現在はこの変更がTeX Live本体に反映されているので、読者諸君はrunscript.tluを編集するのではなくTeX Liveをアップデートするだけでよい)
TeX Live 2018のTeXworksで日本語を名前に含むファイルを開けない問題に対する仮の回避策: C:\texlive\2018\bin\win32\runscript.tlu をエディタで開き、一行目に os.setlocale(“”,”ctype”) を書き込む。
— 新オフトゥン生活 (@mod_poppo) May 8, 2018
TeX Liveのメーリングリストにこの件のメールを投げたところ、Akira Kakuto氏に対応していただいたので、2018年6月9日現在TeX Liveのアップデートを行えば、対策が施されたrunscript.tluが降ってくるはずである。
関連:QA: TeX Live 2018における日本語パスの扱い
余談
5月時点では、Universal CRTの仕様変更
- system関数とかのマルチバイト文字列を受け取る関数がLC_CTYPEの影響を受けるようになったこと
- Universal CRTでLC_CTYPEとしてUTF-8を設定できるようになったこと
に関する資料はWebで見つけられなかった。
setlocale関数のマニュアル(setlocale, _wsetlocale | Microsoft Docs)を見ても仕様変更については書かれていない。
CランタイムをUTF-8に対応させろというFeedback(Create a UTF8 C-runtime – Visual Studio)には動きは見られない。
MicrosoftがCランタイムをUTF-8に対応させようとしているのであればそれは大歓迎だが、黙って仕様変更をした結果今回のTeX Liveのような問題が引き起こされるのは困ったものである。まあ筆者は10年前にメインマシンをMacに乗り換えて幸せなUTF-8生活を送っているのでどうでもいいのだが…。
総括
また筆者の高い問題解決能力を発揮してしまった…。敗北が知りたい
筆者のWindowsと文字コードに関する知識を総動員すればこの程度の問題は2日で片が付くことがわかった。
この筆者の能力を活かせるような(在東京)企業があれば、ぜひ連絡を頂きたい…と言いたいところだがWindowsでプログラミングしても楽しくないのでそういうところは連絡を頂いても嬉しくない…
この記事が面白いと思って頂いた読者のために、筆者のほしい物リスト、ほしい本リストを公開しておく。ちなみに6月は筆者の誕生月である。
(6月10日13時ごろ更新:図を追加、テストプログラムの実行結果をGistへ移動)