共有フォルダから Pharo VM が起動しない件

以前の記事で Pharo VM をビルドする理由として挙げた2つ目の理由は、「共有フォルダに格納したVMが起動できない」ことだった。こちらは日本語名を含んだフォルダから起動しようとした時と異なり、いったん Pharo が起動して画面表示された直後にクラッシュする。

PharoDebug.log を見ると readdir のプリミティブの辺りで落ちているようだったので、ディレクトリの読み込みに失敗していると考えて調べてみた。

ディレクトリの読み込みやファイル属性の処理は FileAttributesPlugin で行っており、最終的には pharo-vm/extracted/plugins/FileAttributesPlugin/src/win/faSupport.c でWindows OSとのやり取りを行っている。

ソースを見るとファイル名に「\\?\」なるプレフィックスを付けているようで、最初は「\\?\」って何?と思った。これは Windows OS で長いファイル名を扱いたい時に使うもので、260文字の制限をなくして 32767文字まで利用できるようになる

faSupport.c では、 fapath 構造体を使って Pharo VM とホストOSとの間のファイル名の橋渡しをしており、path メンバー変数には UTF-8 のファイル名(Pharo VMが使用)を格納し、winpath メンバー変数にはUTF-16のファイル名(Windows で使用)を格納するようになっている。

パス名やディレクトリ名を設定する際にこれらの変換も行っており、UTF-8 のパス名を与えると UTF-16 のパス名を生成するのに加えて、先程の「\\?\」をプレフィックスとして付けたパス名を格納するようになっている。

例えば、UTF-8 の「C:\foo.bar」というファイル名をパス名として設定すると、winpath ではUTF-16の「C:\foo.bar」として参照できるだけでなく、winpathUPP を使ってUTF-16の「\\?\C:\foo.bat」としても参照できるようになる。

共有フォルダも「\\?\」を使って長いファイル名として扱うことができるが、この場合にはプレフィックスとして「\\?\UNC\」を用いる必要がある。つまり、「\\server\share」という共有フォルダの場合には、「\\?\UNC\server\share」と変換しなければならない。

faSupport.c は単純にファイル名にプレフィックスを追加しているだけなので、「\\server\share」は「\\?\\\server\share」と変換されてしまい Windows OS に適切なファイル名として認識されずエラーとなってしまう。これがクラッシュの原因である。

クイックハックとして、共有フォルダが渡された場合には、「\\?\」のプレフィックスを追加せずにそのままのファイル名を使うように変更したところ、クラッシュせず起動できるようになった。(長いので変更部分だけ載せる)

*** faSupport.c.orig	2022-08-18 11:37:17.045418145 +0900
--- faSupport.c.006.ok	2022-08-19 17:46:15.957009495 +0900
***************
*** 36,41 ****
--- 36,42 ----
  sqInt faSetStDir(fapath *aFaPath, char *pathName, int len)
  {
  sqInt	status;
+ int prefixLength;
  	/* Set the St encoded path and ensure trailing delimiter */
  	if (len+1 >= FA_PATH_MAX)
***************
*** 50,60 ****
  	/* Convert to platform specific form
   		Include the \\?\ prefix to allow long path names */
! 	aFaPath->winpathLPP[0] = L'\\';
! 	aFaPath->winpathLPP[1] = L'\\';
! 	aFaPath->winpathLPP[2] = L'?';
! 	aFaPath->winpathLPP[3] = L'\\';
! 	aFaPath->winpath = aFaPath->winpathLPP + 4;
  	status = MultiByteToWideChar(CP_UTF8, 0,
  				aFaPath->path, -1,
  				aFaPath->winpath, FA_PATH_MAX);
--- 51,66 ----
  	/* Convert to platform specific form
   		Include the \\?\ prefix to allow long path names */
! 	if (len > 2 && aFaPath->path[0] == '\\' && aFaPath->path[1] == '\\') {
! 	  prefixLength = 0;
! 	} else {
! 	  aFaPath->winpathLPP[0] = L'\\';
! 	  aFaPath->winpathLPP[1] = L'\\';
! 	  aFaPath->winpathLPP[2] = L'?';
! 	  aFaPath->winpathLPP[3] = L'\\';
! 	  prefixLength = 4;
! 	}
! 	aFaPath->winpath = aFaPath->winpathLPP + prefixLength;
  	status = MultiByteToWideChar(CP_UTF8, 0,
  				aFaPath->path, -1,
  				aFaPath->winpath, FA_PATH_MAX);
***************
*** 62,68 ****
  		return interpreterProxy->primitiveFailForOSError(FA_STRING_TOO_LONG);
  	/* Set aFaPath->uxpath_file and max_file_len to the buffer after the directory */
  	aFaPath->winpathLPP_len = wcslen(aFaPath->winpathLPP);
! 	aFaPath->winpath_len = aFaPath->winpathLPP_len - 4;
  	aFaPath->winpath_file = aFaPath->winpathLPP + aFaPath->winpathLPP_len;
  	aFaPath->winmax_file_len = FA_PATH_MAX - aFaPath->winpath_len;
--- 68,74 ----
  		return interpreterProxy->primitiveFailForOSError(FA_STRING_TOO_LONG);
  	/* Set aFaPath->uxpath_file and max_file_len to the buffer after the directory */
  	aFaPath->winpathLPP_len = wcslen(aFaPath->winpathLPP);
! 	aFaPath->winpath_len = aFaPath->winpathLPP_len - prefixLength;
  	aFaPath->winpath_file = aFaPath->winpathLPP + aFaPath->winpathLPP_len;
  	aFaPath->winmax_file_len = FA_PATH_MAX - aFaPath->winpath_len;
***************
*** 74,79 ****
--- 80,86 ----
  sqInt faSetStPath(fapath *aFaPath, char *pathName, int len)
  {
  sqInt	status;
+ int prefixLength;
  	/* Set the St encoded path */
  	if (len >= FA_PATH_MAX)
***************
*** 86,96 ****
  	/* Convert to platform specific form
   		Include the \\?\ prefix to allow long path names */
! 	aFaPath->winpathLPP[0] = L'\\';
! 	aFaPath->winpathLPP[1] = L'\\';
! 	aFaPath->winpathLPP[2] = L'?';
! 	aFaPath->winpathLPP[3] = L'\\';
! 	aFaPath->winpath = aFaPath->winpathLPP + 4;
  	status = MultiByteToWideChar(CP_UTF8, 0,
  				aFaPath->path, -1,
  				aFaPath->winpath, FA_PATH_MAX);
--- 93,108 ----
  	/* Convert to platform specific form
   		Include the \\?\ prefix to allow long path names */
! 	if (len > 2 && aFaPath->path[0] == '\\' && aFaPath->path[1] == '\\') {
! 	  prefixLength = 0;
! 	} else {
! 	  aFaPath->winpathLPP[0] = L'\\';
! 	  aFaPath->winpathLPP[1] = L'\\';
! 	  aFaPath->winpathLPP[2] = L'?';
! 	  aFaPath->winpathLPP[3] = L'\\';
! 	  prefixLength = 4;
! 	}
! 	aFaPath->winpath = aFaPath->winpathLPP + prefixLength;
  	status = MultiByteToWideChar(CP_UTF8, 0,
  				aFaPath->path, -1,
  				aFaPath->winpath, FA_PATH_MAX);
***************
*** 98,104 ****
  		return interpreterProxy->primitiveFailForOSError(FA_STRING_TOO_LONG);
  	/* Set aFaPath->uxpath_file and max_file_len to the buffer after the directory */
  	aFaPath->winpathLPP_len = wcslen(aFaPath->winpathLPP);
! 	aFaPath->winpath_len = aFaPath->winpathLPP_len - 4;
  	aFaPath->winpath_file = 0;
  	aFaPath->winmax_file_len = 0;
--- 110,116 ----
  		return interpreterProxy->primitiveFailForOSError(FA_STRING_TOO_LONG);
  	/* Set aFaPath->uxpath_file and max_file_len to the buffer after the directory */
  	aFaPath->winpathLPP_len = wcslen(aFaPath->winpathLPP);
! 	aFaPath->winpath_len = aFaPath->winpathLPP_len - prefixLength;
  	aFaPath->winpath_file = 0;
  	aFaPath->winmax_file_len = 0;
***************
*** 131,136 ****
--- 143,149 ----
+ #if 0
  /*
   * faSetPlatPath
   *
***************
*** 174,203 ****
  	return 0;
  }
  sqInt faSetPlatPathOop(fapath *aFaPath, sqInt pathNameOop)
  {
  int	byteCount;
! char	*bytePtr;
  int	len;
  	byteCount = interpreterProxy->stSizeOf(pathNameOop);
! 	bytePtr = interpreterProxy->arrayValueOf(pathNameOop);
  	len = byteCount / sizeof(WCHAR);
  	if (len >= FA_PATH_MAX)
  		return interpreterProxy->primitiveFailForOSError(FA_STRING_TOO_LONG);
! 	aFaPath->winpathLPP[0] = L'\\';
! 	aFaPath->winpathLPP[1] = L'\\';
! 	aFaPath->winpathLPP[2] = L'?';
! 	aFaPath->winpathLPP[3] = L'\\';
! 	aFaPath->winpath = aFaPath->winpathLPP + 4;
! 	memcpy(aFaPath->winpath, bytePtr, byteCount);
  	aFaPath->winpath[len] = 0;
  	aFaPath->winpath_len = len;
! 	aFaPath->winpathLPP_len = len + 4;
  	aFaPath->winpath_file = 0;
  	aFaPath->winmax_file_len = 0;
--- 187,223 ----
  	return 0;
  }
+ #endif
  sqInt faSetPlatPathOop(fapath *aFaPath, sqInt pathNameOop)
  {
  int	byteCount;
! LPWSTR	wcharPtr;
  int	len;
+ int prefixLength;
  	byteCount = interpreterProxy->stSizeOf(pathNameOop);
! 	wcharPtr = (LPWSTR) interpreterProxy->arrayValueOf(pathNameOop);
  	len = byteCount / sizeof(WCHAR);
  	if (len >= FA_PATH_MAX)
  		return interpreterProxy->primitiveFailForOSError(FA_STRING_TOO_LONG);
! 	if (len > 2 && wcharPtr[0] == '\\' && wcharPtr[1] == '\\') {
! 	  prefixLength = 0;
! 	} else {
! 	  aFaPath->winpathLPP[0] = L'\\';
! 	  aFaPath->winpathLPP[1] = L'\\';
! 	  aFaPath->winpathLPP[2] = L'?';
! 	  aFaPath->winpathLPP[3] = L'\\';
! 	  prefixLength = 4;
! 	}
! 	aFaPath->winpath = aFaPath->winpathLPP + prefixLength;
! 	memcpy(aFaPath->winpath, wcharPtr, byteCount);
  	aFaPath->winpath[len] = 0;
  	aFaPath->winpath_len = len;
! 	aFaPath->winpathLPP_len = len + prefixLength;
  	aFaPath->winpath_file = 0;
  	aFaPath->winmax_file_len = 0;

共有フォルダでも通常のファイルと同じように、プレフィックスを付けたパスと、そうでないパスの両者を管理するようにしたいと思ったが、fapath 構造体の定義を変更することなしに行うのは難しいようだ。しかし、上記のクイックハックだと長いファイル名に対応できないので、共有フォルダの場合は「\\?\UNC\」プレフィックスを付けたものだけを格納するようにした方がいいのかもしれない。

どちらを採用するのかはお盆休み開けにターゲット環境で試してみて決めようと思う。