All Green! (DBMError: dbm_store failed in ruby1.8.7_p160 その5)

link関数を呼んでいる以上、コピーではなくやはりリンクを使うべきだろうということで、CopyFileの代わりにWindows 2000以降実装されたCreateHardLinkで試してみることにした。

CreateHardLinkのマクロはCopyFile同様にVC++に付属しているWinBase.h(%VSINSTALLDIR%\VC\PlatformSDK\Include\WinBase.h)に定義されている。

  • WinBase.h (Line:7698〜7725)
#if (_WIN32_WINNT >= 0x0500)
//
// API call to create hard links.
//

WINBASEAPI
BOOL
WINAPI
CreateHardLinkA(
    __in       LPCSTR lpFileName,
    __in       LPCSTR lpExistingFileName,
    __reserved LPSECURITY_ATTRIBUTES lpSecurityAttributes
    );
WINBASEAPI
BOOL
WINAPI
CreateHardLinkW(
    __in       LPCWSTR lpFileName,
    __in       LPCWSTR lpExistingFileName,
    __reserved LPSECURITY_ATTRIBUTES lpSecurityAttributes
    );
#ifdef UNICODE
#define CreateHardLink  CreateHardLinkW
#else
#define CreateHardLink  CreateHardLinkA
#endif // !UNICODE

#endif // (_WIN32_WINNT >= 0x0500)

これをそのまま使えば良い訳だが、注意しなければならないのがマクロ_WIN32_WINNTである。VC++ 2005に添付されているWinBase.hのデフォルトでは0x0400(Windows 95/NT)が定義されているため、そのままでは上記のマクロが展開されない。

従ってプリプロセッサ等で"_WIN32_WINNT"の値を適切に設定する必要がある。

_WIN32_WINNTの一覧(http://msdn.microsoft.com/ja-jp/library/aa383745.aspx)

OS _WIN32_WINNTの値
Windows Server 2008 0x0600
Windows Vista 0x0600
Windows Server 2003 with SP1, Windows XP with SP2 0x0502
Windows Server 2003, Windows XP 0x0501
Windows 2000 0x0500
今回はMakeFileに直接マクロの設定を挿入してしまうことにした。
LOCAL_DEFS = -DBUILDING_DLL -D_WIN32_WINNT=0x0600 
ではwin32.cを書き換えてビルドしてみよう。変更に関しては後でパッチを当てられるようにdiff形式で書いておく。
  • win32c.diff
@@ -14,7 +14,7 @@
 
 int link(const char *src, const char *dst)
 {
-    return !CopyFile(src, dst, TRUE);
+    return !CreateHardLink(dst, src, NULL);
 }
 
CopyFileと比べると引数の型が文字列へのポインタであることは変わらないが、順が逆になることに注意が必要だ。 ビルドはすんなり通った。
E:\ruby_build_image\gdbm-1.8.3-1\win32>nmake clean all

Microsoft(R) Program Maintenance Utility Version 8.00.50727.762
Copyright (C) Microsoft Corporation.  All rights reserved.
:
:
        cl -nologo -c  -DBUILDING_DLL -D_WIN32_WINNT=0x0600  -I.. -I..\.. -I.\win32\include  -MD gdbm.c
gdbm.c
:
:
        cl -nologo -LD gdbm.obj bucket.obj close.obj dbmclose.obj dbmdelete.obj dbmdirfno.obj  dbmfetch.obj dbminit.obj dbmopen.obj dbmpagfno.obj dbmrdonly.obj  dbmseq.obj dbmstore.obj delete.obj falloc.obj fetch.obj findkey.obj  gdbmclose.obj gdbmdelete.obj gdbmerrno.obj gdbmexists.obj  gdbmfdesc.obj gdbmfetch.obj gdbmopen.obj gdbmreorg.obj gdbmseq.obj  gdbmsetopt.obj gdbmstore.obj gdbmsync.obj global.obj hash.obj  seq.obj store.obj update.obj version.obj win32.obj  -Fegdbm.dll
   ライブラリ gdbm.lib とオブジェクト gdbm.exp を作成中
gdbm.lib gdbm.dll
        cd ..

E:\ruby_build_image\gdbm-1.8.3-1\win32>nmake install
:
なお、このMakeFileマニフェストの埋込は行われないので、都度mt.exeを使うかMakeFileにターゲットを追加してSxS用のマニフェストをgdbm.dllに埋め込む必要がある。
>mt -manifest gdbm.dll.manifest -outputresource:gdbm.dll;2
あとはビルドで出来たgdbm.libとヘッダファイルを使って、以前書いたのと同様にdbm.soとgdbm.soを作り直せば作業は完了だ。(予めdbm/win32、gdbm/win32というディレクトリに生成物をコピーしておく)
  • extconf.rbを実行してMakeFileを生成する (dbm.so)
E:\ruby-1.8.7-p160\ext\dbm>ruby extconf.rb --with-winsock2 --with-dbm-lib=win32\lib --with-dbm-include=win32\include
checking for __db_ndbm_open() in db.lib... no
checking for __db_ndbm_open()... no
checking for __db_ndbm_open() in db2.lib... no
checking for __db_ndbm_open()... no
checking for dbm_open() in db1.lib... no
checking for dbm_open()... no
checking for dbm_open() in dbm.lib... no
checking for dbm_open()... no
checking for dbm_open() in gdbm.lib... yes
checking for DBM in gdbm-ndbm.h... no
checking for DBM in ndbm.h... yes
checking for cdefs.h... no
checking for sys/cdefs.h... no
creating Makefile
  • nmakeを使ってビルド、インストールする (dbm.so)
E:\ruby-1.8.7-p160\ext\dbm>nmake

Microsoft(R) Program Maintenance Utility Version 8.00.50727.762
Copyright (C) Microsoft Corporation.  All rights reserved.

        e:\ruby\bin\ruby -e "puts 'EXPORTS', 'Init_dbm'"  > dbm-i386-mswin32_80.def
Setting environment for using Microsoft Visual Studio 2005 x86 tools.
        cl -nologo -I. -I. -Ie:/ruby/lib/ruby/1.8/i386-mswin32_80 -I. -MD   -O2b2xty- -DHAVE_TYPE_DBM -DDBM_HDR="" -Iwin32\include -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE -c -Tcdbm.c
Setting environment for using Microsoft Visual Studio 2005 x86 tools.
dbm.c
        cl -nologo -LD -Fedbm.so dbm.obj msvcr80-ruby18.lib gdbm.lib  oldnames.lib user32.lib advapi32.lib shell32.lib ws2_32.lib   -link -incremental:no -debug -opt:ref -opt:icf -dll -libpath:"." -libpath:"e:/ruby/lib" -libpath:"win32\lib"  -implib:dbm-i386-mswin32_80.lib -pdb:dbm-i386-mswin32_80.pdb -def:dbm-i386-mswin32_80.def
   ライブラリ dbm-i386-mswin32_80.lib とオブジェクト dbm-i386-mswin32_80.exp を作成中
        mt -nologo -manifest dbm.so.manifest -outputresource:dbm.so;2

E:\ruby-1.8.7-p160\ext\dbm>nmake install

Microsoft(R) Program Maintenance Utility Version 8.00.50727.762
Copyright (C) Microsoft Corporation.  All rights reserved.

install -c -p -m 0755 dbm.so e:\ruby\lib\ruby\site_ruby\1.8\i386-msvcr80

さて、いよいよだ。test_dbm.rbを実行してみよう。 ヤッターーーーーーーーーーーーーーーーーーーーーーーーーーー ちなみにtest_gdbm.rbも元々失敗していた二つのテスト(test_reorganizeとtest_s_open_create_new)以外は全てグリーンであることを確認できた。 やっと全てのテストが通った。これが何かの役に立つとは思えないけど(今更DBM、それもWindows上での需要があるとは思えない)、諦めずにいろいろとやってきて良かった。とはいえ、知っている方であれば一日もたたずに完了した作業を数日間かけている当たり、やはりこの辺は知らないことが多いことを露呈した恰好だ。 追記: CopyFileの代わりにCreateHardLinkを使うことでテストが通った訳だが、問題が無い訳ではない。というのもCreateHardLinkを使う際には制限が存在しているからだ。
  • CreateHardLinkの制限
    • Windows 2000以降のO/Sを使用している
    • 対象ファイルが属するボリュームはNTFSフォーマットされている
Windows2000以降は良いとして、NTFS縛りは最悪問題が出ることも考えられるので、同じような対応を行う場合は注意が必要だ。今は思いつかないが、回避策を含めたコードで書き換えたいところ。
  • テストコードは偉大だ
今回のDBMのように「どうなっていれば正しい」のかが私には分からないような場合、テストコードがあるというのは本当に助かる。テストが引っかかっていなければdbm.soを作った所で満足していたことだろう。 なお、test_dbm.rbだがテストに途中で失敗した場合にセットアップで作ったデータベースファイルが消えないため、次回のテストが失敗することが分かったので、自分の環境では以下の修正を施している。
  • test_dbm.rb.diff
@@ -40,13 +40,16 @@
       assert_instance_of(DBM, @dbm_rdonly = DBM.new("tmptest_dbm_rdonly", nil))
     end
     def teardown
-      assert_nil(@dbm.close)
-      assert_nil(@dbm_rdonly.close)
-      ObjectSpace.each_object(DBM) do |obj|
-        obj.close unless obj.closed?
+      begin
+        assert_nil(@dbm.close)
+        assert_nil(@dbm_rdonly.close)
+        ObjectSpace.each_object(DBM) do |obj|
+          obj.close unless obj.closed?
+        end
+      ensure
+        File.delete *Dir.glob("tmptest_dbm*").to_a
+        p Dir.glob("tmptest_dbm*") if $DEBUG
       end
-      File.delete *Dir.glob("tmptest_dbm*").to_a
-      p Dir.glob("tmptest_dbm*") if $DEBUG
     end