しょんぼり技術メモ

まいにちがしょんぼり

Dokan .netバインディングの勉強

VMware上でのデバッグ方法がわかったので、実際にコードを書いてみる。
C#スキーなので、Dokanの.netバインディングを使う。

プロジェクトに追加

DokanNetディレクトリにある、"DokanNet.cs", "DokanOperations.cs", "Proxy.cs" をプロジェクトに追加しておく。

まずはDokanOperationsを実装する

Dokan.netで実装しなければいけない機能は、すべてDokan.DokanOperationsというインタフェースにまとめられている。

なので、コンソールプログラムを選択した際に自動生成されたコードを次のように変更する。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Dokan;

namespace DokanTest
{
    class Program : Dokan.DokanOperations
    {

これで、ProgramクラスはDokanOperationsインタフェースを実装することになる。
VisualStudioの機能で、DokanOperationsインタフェースの定義を実装する(メンバ関数定義を自動記述する)ことができるので、この機能を使って一気に関数を記述すると、次のようになる。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Dokan;

namespace DokanTest
{
    class Program : Dokan.DokanOperations
    {
        static void Main(string[] args)
        {
        }

        #region DokanOperations メンバ

        public int CreateFile(string filename, System.IO.FileAccess access, System.IO.FileShare share, System.IO.FileMode mode, System.IO.FileOptions options, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int OpenDirectory(string filename, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int CreateDirectory(string filename, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int Cleanup(string filename, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int CloseFile(string filename, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int ReadFile(string filename, byte[] buffer, ref uint readBytes, long offset, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int WriteFile(string filename, byte[] buffer, ref uint writtenBytes, long offset, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int FlushFileBuffers(string filename, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int GetFileInformation(string filename, FileInformation fileinfo, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int FindFiles(string filename, System.Collections.ArrayList files, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int SetFileAttributes(string filename, System.IO.FileAttributes attr, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int SetFileTime(string filename, DateTime ctime, DateTime atime, DateTime mtime, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int DeleteFile(string filename, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int DeleteDirectory(string filename, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int MoveFile(string filename, string newname, bool replace, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int SetEndOfFile(string filename, long length, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int LockFile(string filename, long offset, long length, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int UnlockFile(string filename, long offset, long length, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int GetDiskFreeSpace(ref ulong freeBytesAvailable, ref ulong totalBytes, ref ulong totalFreeBytes, DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        public int Unmount(DokanFileInfo info)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

あとは、これらの関数のうち必要なものを実装していけばよい。

何もしないファイルシステムを作る

まずは、何もしないファイルシステムを作ってみる。

マウントを行うためには、Main()関数でマウントの命令を実行する必要がある。

namespace DokanTest
{
    class Program : Dokan.DokanOperations
    {
        const string default_mount_drive = "x";

        static void Main(string[] args)
        {
            // arg[0]: マウントされるドライブのドライブレター

            // オプション設定
            DokanOptions opt = new DokanOptions();
            opt.DebugMode = true;
            opt.DriveLetter = (args.Length == 1) ? args[0].ToCharArray()[0] : default_mount_drive.ToCharArray()[0];
            opt.ThreadCount = 1;
            opt.VolumeLabel = "DokanTest";

            // マウント実行
            DokanNet.DokanMain(opt, new Program());
        }

そして、各関数にある

throw new NotImplementedException();

をなんとかする。今回はめんどくさいので、全て

return 0;

に置換した。


これで実行してみると、X:ドライブにDokanTestというボリュームラベルを持つドライブが見えるようになる。
実際の関数は全く実装していないため、ファイルの作成や空きディスク領域の取得などは全く機能しないが、ファイルシステムとして動作していることが確認できる。


なお、GetFileInformation()で何もせずreturn 0;しているため、次のようなエラーが出る:

System.ArgumentOutOfRangeException: 有効な Win32 FileTime ではありません。
   場所 System.DateTime.ToFileTimeUtc()
   場所 System.DateTime.ToFileTime()
   場所 Dokan.Proxy.GetFileInformationProxy(IntPtr FileName, BY_HANDLE_FILE_INFORMATION&
   HandleFileInformation, DOKAN_FILE_INFO& FileInfo) 場所 Proxy.cs:行 436

正しい情報を一切返していないので、ある意味当然の結果である。必要な機能は、追々追加していくことにする。

Hello worldファイルシステムを作る

元ネタ:InfoQ: ファイルシステムでHello World


Ruby版の簡潔さが嘘のよう。文字列をstringからbyte[]にしなきゃいけなかったり、ReadFileでoffsetがファイルサイズを超えている場合に対処しなきゃいけなかったり。


CreateFile()関数は、名前とは裏腹に、作成以外のオペレーションでもあっちこっちで呼ばれる。全関数にDebugOutがついているのは、動作を勉強するためのもの。普段の何気ないエクスプローラでの操作でも、大量の関数呼び出しが行われることがわかる。


以下ソース。見づらくても泣かない。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Dokan;

namespace DokanTest
{
    class Program : Dokan.DokanOperations
    {
        // デフォルトのドライブレター
        const string default_mount_drive = "x";


        // コンソールにデバッグ文字列を流すか
        const bool DEBUGMODE = true;
        // →デバッグ文字列をファイルにも吐き出すか
        const bool DEBUG_WRITE_TO_FILE = false;
        // →→ログファイルのパス
        const string debug_log_file = "c:\\dokan.log";


        // 見せるファイルのファイル名
        const string FNAME_HELLO_WORLD = "hello.txt";
        // 見せるファイルの中身
        const string STR_HELLO_WORLD = "Hello world!\r\n";


        static void Main(string[] args)
        {
            // arg[0]: マウントされるドライブのドライブレター

            // オプション設定
            DokanOptions opt = new DokanOptions();
            opt.DebugMode = true;
            opt.DriveLetter = (args.Length == 1) ? args[0].ToCharArray()[0] : default_mount_drive.ToCharArray()[0];
            opt.ThreadCount = 1;
            opt.VolumeLabel = "DokanTest";

            Console.WriteLine("Mounting Dokan...");


            // マウント実行
            DokanNet.DokanMain(opt, new Program());
        }


        /// <summary>
        /// デバッグ出力
        /// </summary>
        private void DebugOut(string mes)
        {
            // コンソールへのデバッグ出力(時刻表示付き)
            if (DEBUGMODE)
            {
                string message = string.Format("[{0}] {1}", System.DateTime.Now.ToLongTimeString(), mes);
                Console.WriteLine(message);
                
                // ファイル出力
                if (DEBUG_WRITE_TO_FILE)
                {
                    // 面倒なので毎回開いて追記して閉じる。恐らくスループットがた落ちのはず
                    using (System.IO.StreamWriter sw = new System.IO.StreamWriter(debug_log_file, true, Encoding.Default))
                    {
                        sw.WriteLine(message);
                    }
                }
            }
        }



        #region DokanOperations メンバ

        public int CreateFile(string filename, System.IO.FileAccess access, System.IO.FileShare share, System.IO.FileMode mode, System.IO.FileOptions options, DokanFileInfo info)
        {
            DebugOut("CreateFile() called with filename: " + filename);

            // ルートディレクトリを特別扱い
            if (filename == "\\")
            {
                info.IsDirectory = true;
                return DokanNet.DOKAN_SUCCESS;
            }
            // hello.txtのみ許可
            else if (filename == "\\" + FNAME_HELLO_WORLD)
            {
                return DokanNet.DOKAN_SUCCESS;
            }

            // ほかはFILE_NOT_FOUND
            return -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        public int OpenDirectory(string filename, DokanFileInfo info)
        {
            DebugOut("OpenDirectory() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int CreateDirectory(string filename, DokanFileInfo info)
        {
            DebugOut("CreateDirectory() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int Cleanup(string filename, DokanFileInfo info)
        {
            DebugOut("Cleanup() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int CloseFile(string filename, DokanFileInfo info)
        {
            DebugOut("CloseFile() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int ReadFile(string filename, byte[] buffer, ref uint readBytes, long offset, DokanFileInfo info)
        {
            DebugOut("ReadFile() called with filename:    " + filename);
            DebugOut("                  with buffer size: " + buffer.Length.ToString());
            DebugOut("                  with offset     : " + offset.ToString());

            // hello.txt のみ処理の対象にする
            if (filename == "\\" + FNAME_HELLO_WORLD)
            {
                // "Hello world!" のバイト配列表現を取得
                byte[] ret = System.Text.Encoding.Default.GetBytes(STR_HELLO_WORLD);

                // offsetが"Hello world!"のサイズよりも大きければ何も読み込まない
                if (offset >= ret.Length)
                {
                    DebugOut("                  offset exceeds \"Hello world\"'s length.");

                    readBytes = 0;
                    return -DokanNet.DOKAN_ERROR;
                }

                // バッファサイズの方が大きければ何も気にせずコピーする
                if (ret.Length <= buffer.Length)
                {
                    // バッファにコピー
                    ret.CopyTo(buffer, 0);
                    // 文字数をreadBytesに格納
                    readBytes = (uint)ret.Length;

                    DebugOut("                  set readBytes as: " + readBytes.ToString());

                    return DokanNet.DOKAN_SUCCESS;
                }
            }

            return -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        public int WriteFile(string filename, byte[] buffer, ref uint writtenBytes, long offset, DokanFileInfo info)
        {
            DebugOut("WriteFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int FlushFileBuffers(string filename, DokanFileInfo info)
        {
            DebugOut("FlushFileBuffers() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int GetFileInformation(string filename, FileInformation fileinfo, DokanFileInfo info)
        {
            DebugOut("GetFileInformation() called with filename: " + filename);

            // hello.txt のみ処理の対象にする
            if (filename == "\\" + FNAME_HELLO_WORLD)
            {
                // ファイル情報を適当に設定する
                fileinfo.Attributes = System.IO.FileAttributes.Normal;
                fileinfo.CreationTime = System.DateTime.Now;
                fileinfo.LastAccessTime = System.DateTime.Now;
                fileinfo.LastWriteTime = System.DateTime.Now;
                fileinfo.Length = System.Text.Encoding.Default.GetBytes(STR_HELLO_WORLD).Length;

                return DokanNet.DOKAN_SUCCESS;
            }
            // ルートディレクトリは特別扱い
            else if(filename == "\\")
            {
                // ルートディレクトリ情報を適当に設定する
                fileinfo.Attributes = System.IO.FileAttributes.Directory;
                fileinfo.CreationTime = System.DateTime.Now;
                fileinfo.LastAccessTime = System.DateTime.Now;
                fileinfo.LastWriteTime = System.DateTime.Now;
                fileinfo.Length = 0;

                return DokanNet.DOKAN_SUCCESS;
            }

            return -DokanNet.DOKAN_ERROR;
        }

        public int FindFiles(string filename, System.Collections.ArrayList files, DokanFileInfo info)
        {
            DebugOut("FindFiles() called with filename: " + filename);

            // hello.txt のみを適当に設定する
            FileInformation fi = new FileInformation();
            fi.FileName = FNAME_HELLO_WORLD;
            fi.Length = System.Text.Encoding.Default.GetBytes(STR_HELLO_WORLD).Length;
            fi.Attributes = System.IO.FileAttributes.Normal;
            fi.CreationTime = System.DateTime.Now;
            fi.LastAccessTime = System.DateTime.Now;
            fi.LastWriteTime = System.DateTime.Now;

            // ファイル一覧に追加する
            files.Add(fi);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int SetFileAttributes(string filename, System.IO.FileAttributes attr, DokanFileInfo info)
        {
            DebugOut("SetFileAttributes() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int SetFileTime(string filename, DateTime ctime, DateTime atime, DateTime mtime, DokanFileInfo info)
        {
            DebugOut("SetFileTime() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int DeleteFile(string filename, DokanFileInfo info)
        {
            DebugOut("DeleteFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int DeleteDirectory(string filename, DokanFileInfo info)
        {
            DebugOut("DeleteDirectory() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int MoveFile(string filename, string newname, bool replace, DokanFileInfo info)
        {
            DebugOut("MoveFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int SetEndOfFile(string filename, long length, DokanFileInfo info)
        {
            DebugOut("SetEndOfFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int LockFile(string filename, long offset, long length, DokanFileInfo info)
        {
            DebugOut("LockFile() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int UnlockFile(string filename, long offset, long length, DokanFileInfo info)
        {
            DebugOut("UnlockFile() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int GetDiskFreeSpace(ref ulong freeBytesAvailable, ref ulong totalBytes, ref ulong totalFreeBytes, DokanFileInfo info)
        {
            DebugOut("GetDiskFreeSpace() called.");

            freeBytesAvailable = 0;
            totalBytes = (ulong)System.Text.Encoding.Default.GetBytes("Hello world!\n").Length;
            totalFreeBytes = 0;

            return DokanNet.DOKAN_SUCCESS;
        }

        public int Unmount(DokanFileInfo info)
        {
            DebugOut("Unmount() called.");

            return DokanNet.DOKAN_SUCCESS;
        }

        #endregion
    }
}

できたー。

苦労した点。

CreateFile()関数はファイル作成以外でも超いっぱい呼ばれる。しかもルートディレクトリ("\")相手でも平気で呼ばれまくる。ヤバい。
しかも無碍に return -DokanNet.DOKAN_ERROR; とかやると「パラメータが間違っています。」とか言われる。これはひどい
なので、ルートディレクトリ"\"と見せてあげたいファイルそれぞれへのアクセスの場合には、ちゃんと return DokanNet.DOKAN_SUCCESS; してあげないとダメっぽい。


メモ帳やcmd.exeのtypeコマンドなどは、"Hello world!\r\n"の文字数分、つまり14文字分読み込んだらそこで読み込みが終了する。しかし、秀丸やワードパッドなどは、どうも読み込みが行える限り読み込みを行おうとするようで、何も気にせずバッファにコピーすると無限にファイルを読み込もうとしてしまっておもしろいことになってしまう。
そこで、ReadFile()関数でoffsetの値をチェックし、実体のサイズを超えるような場合にはエラーを返すようにすればこの問題は解決するようだ。(これはファイルシステムとしては常識なのかも?)


思いの外手こずってしまった。しかし、ちゃんとファイルの中身が表示されたときは、なんか楽しいものだ。今後もいろいろとDokanで遊んでみよう。