2019年09月01日

wxLuaApp: Windows の日本語ファイル名に苦戦

 wxWidgets アプリを Lua で記述する wxLuaApp。wxWidgets 3.0 系列でビルドできるようにするパッチをPaul Kulchenkoさんに送ったら、採用してもらった。これを機会に、こちらのプロジェクトの構成も少し整理して、開発を続けている。

 Mac でだいたい動くようになったので、Windows でもビルドしてみた。動くには動くんだけど、日本語のファイル名が通らない。調べてみると、Windows の fopen 関数は、マルチバイト文字のファイル名に対応していない。文字列を wchar_t の配列に変換して、_wfopen 関数を使うように指定されている。

Fopen関数は、 filenameによって指定されたファイルを開きます。 既定では、ナローファイル名の文字列は、ANSI コードページ (CP_ACP) を使用して解釈されます。 ... _wfopenは、 fopenのワイド文字バージョンです。 _wfopenの引数はワイド文字列です。 それ以外の場合、 _wfopenfopenは同じように動作します。

fopen, _wfopen: Visual Studio 2019 の関数リファレンス

 対策は、原理的にはそれほど難しくない。自前の fopen 関数を書いて、UTF-8 で書かれたファイル名をマルチバイト文字列に置き換えて、_wfopen を呼べばいい。コーディングも難しくない。

/*  win_fopen.c  */
/*  2019.8.31. Toshi Nagata  */
/*  Public Domain  */

#if _WIN32 || _WIN64
#include <Windows.h>
#include <stdio.h>
static int utf8towchar(const char *utf8, wchar_t **outbuf)
{
  size_t buflen = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, (void *)0, 0);
  wchar_t *buf = calloc(buflen, sizeof(wchar_t));
  if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, buf, buflen) == 0) {
    free(buf);
    return -1;
  }
  *outbuf = buf;
  return 0;
}

FILE *
fopen(const char *path, const char *mode)
{
  wchar_t *wpath, *wmode;
  FILE *f;
  if (utf8towchar(path, &wpath) != 0 || utf8towchar(mode, &wmode) != 0)
    return NULL;
  f = _wfopen(wpath, wmode);
  free(wpath);
  free(wmode);
  return f;
}
#endif

 大苦戦したのは、これをどうやって組み込むか。wxLuaApp は、LuaJIT 2.0.5 の動的ライブラリとリンクしている。LuaJIT の中から fopen を呼び出しているところがいくつもあるので、これをどうやって自前の関数で置き換えるかが、なかなかわからなかった。試行錯誤の結果、次のやり方でうまくいくことがわかった。

  • win_fopen.c をコンパイルして win_fopen.o を作っておく。
  • LuaJIT の動的ライブラリ lua51.dll のリンクの時に win_fopen.o をオブジェクトファイルとして追加する。LuaJIT の make を走らせる時に、TARGET_SHLDFLAGS="path/to/winfopen.o" を引数として指定すればよい。
  • wxLuaApp をビルドする時にも、win_fopen.o をリンクする。

 DLL とアプリ本体の両方を win_fopen.o とリンクするのがポイントだった。win_fopen.o のコードがダブってしまうが、これは止むを得ない。(wxLuaApp 本体が lua51.dll の中の fopen を使ってくれればいいのだが、どうやっても msvcrt.dllfopen が優先されてしまう。リンクの順序を変えてもうまくいかなかった。)

 なお、DLL やアプリ本体の .exe ファイルなどが、どの動的ライブラリのどの関数を呼び出しているかは、objdump を使うとわかる。-p オプションを使うと、下のように動的ライブラリごとに使用する関数が出力される。

$ x86_64-w64-mingw32-objdump -p lua51.dll | less
        ...
        DLL Name: msvcrt.dll
        vma:  Hint/Ord Member-Name Bound-To
        679f8      59  __DestructExceptionObject
        67a14      80  __doserrno
        67a22      84  __iob_func
        67a30      95  __pioinfo
        67a3c     126  _amsg_exit
        ...
        67ce0    1235  tmpfile
        67cea    1237  tmpnam
        67cf4    1243  ungetc
        67cfe    1245  vfprintf

 msvcrt.dll 由来の関数の中に fopen があると、日本語ファイル名が正しく開けない。正しく対策すると、msvcrt.dll の関数リストから fopen が消える。

Posted at 2019年09月01日 10:36:13
email.png