...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

D言語でMicrosoft Component Object Model (COM) を使う (その7)

D言語 Advent Calendar 2025 11日目の記事です。

その1でCOMインタフェースを使うために必要な宣言や定義の準備ができて、 その2でCOMの初期化と解放ができるようになりました。 その3では実際にファイルを開くダイアログを表示することができ、その4では選択したファイルのパスを取得できました。 その5ではQueryInterfaceを使って別のCOMインタフェースを取得して、ファイル作成日時を取得できました。 その6ではCOMインタフェースをアップキャストもダウンキャストもできないことを説明しました。 ということで、ここまでは「COM使う編」とします。

ここからは「COM作る編」です。

独自のCOMオブジェクトを作りたい

もう一度、ファイル選択ダイアログを開いてファイルを選んで、そのファイルの作成日時を得るコードを掲載します。 ソース全文はその5に書いてあるので、ここでは前半の宣言と定義は省略します。

void main()
{
    HRESULT hr;

    hr = CoInitializeEx(null, COINIT.COINIT_MULTITHREADED);
    if (FAILED(hr)) return;
    scope(exit) CoUninitialize();

    IFileOpenDialog pDialog;
    hr = CoCreateInstance(&CLSID_FileOpenDialog, null, CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, cast(void**)&pDialog);
    if (FAILED(hr)) return;
    scope(exit) pDialog.Release();

    hr = pDialog.Show(null);
    if (FAILED(hr)) return;

    IShellItem pItem;
    hr = pDialog.GetResult(&pItem);
    if (FAILED(hr)) return;
    scope(exit) pItem.Release();

    LPWSTR outPath;
    hr = pItem.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, &outPath);
    if (FAILED(hr)) return;
    scope(exit) CoTaskMemFree(outPath);

    import std.conv;
    writeln(outPath.to!string);

    IShellItem2 pItem2;
    hr = pItem.QueryInterface(&IID_IShellItem2, cast(void**)&pItem2);
    if (FAILED(hr)) return;
    scope(exit) pItem2.Release();

    PROPERTYKEY prop = PKEY_DateCreated;
    FILETIME filetime;
    SYSTEMTIME time;
    pItem2.GetFileTime(&prop, &filetime);
    FileTimeToSystemTime(&filetime, &time);
    writefln("%s-%s-%s", time.wYear, time.wMonth, time.wDay);
}

やりたいことは割とシンプルなのに、複数のCOMインタフェースを使っています。 ファイル選択ダイアログで選んだファイルの作成日時を取得するだけのCOMオブジェクトとCOMインタフェースがあれば、 コードがシンプルになりそうです。

そういうCOMオブジェクトとCOMインタフェースを作ります。

どんなCOMオブジェクトとCOMインタフェースを作るか

IFilePicker COMインタフェースは次のとおりにしました。 pickFileメソッドにパスと時間を受け取る変数を与える形です。

interface IFilePicker : IUnknown
{
extern (Windows):
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time);
}

D言語では、COMインタフェースの基底クラスはIUnknownインタフェースと決まっています。

ここでIUnknownインタフェースはどういうものかというと、 core.sys.windows.unknwnで次のとおり宣言されています。

extern (Windows) {

    interface IUnknown {
        HRESULT QueryInterface(IID* riid, void** pvObject);
        ULONG AddRef();
        ULONG Release();
    }
// ...

そうすると、IFilePickerを実装したCOMオブジェクトのひな形は、こんな感じになります。 COMオブジェクトはインタフェースではなく実装を持つので、classで定義します。

class FilePicker : IFilePicker // ...
{
extern (Windows):
    HRESULT QueryInterface(IID* riid, void** pvObject) { ... }
    ULONG AddRef() { ... }
    ULONG Release() { ... }
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time) { ... }
}

ComObjectを継承する

実はAddRefとReleaseは定型文になるので、 COMオブジェクトを簡単に実装するための基底クラスとして、 core.sys.windows.comモジュールでComObjectが用意されています。 その実装は次のとおりです。

extern (Windows)
{

class ComObject : IUnknown
{
extern (Windows):
    HRESULT QueryInterface(const(IID)* riid, void** ppv)
    {
        if (*riid == IID_IUnknown)
        {
            *ppv = cast(void*)cast(IUnknown)this;
            AddRef();
            return S_OK;
        }
        else
        {   *ppv = null;
            return E_NOINTERFACE;
        }
    }

    ULONG AddRef()
    {
        return atomicOp!"+="(*cast(shared)&count, 1);
    }

    ULONG Release()
    {
        LONG lRef = atomicOp!"-="(*cast(shared)&count, 1);
        if (lRef == 0)
        {
            // free object

            // If we delete this object, then the postinvariant called upon
            // return from Release() will fail.
            // Just let the GC reap it.
            //delete this;

            return 0;
        }
        return cast(ULONG)lRef;
    }

    LONG count = 0;             // object reference count
}

}

AddRefとReleaseはComObjectのものをそのまま使うことにして、 FilePickerクラスの定義を次のとおり修正します。 QueryInterfaceメソッドは自分で実装しなければならないので、overrideします。

class FilePicker : ComObject, IFilePicker
{
extern (Windows):
    override HRESULT QueryInterface(IID* riid, void** pvObject) { ... }
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time) { ... }
}

CLSIDとIIDのためにuuidを生成する

続いて、独自のCOMオブジェクトを自作するので、そのCOMオブジェクトに唯一対応するCLSIDを定義する必要があります。 また、独自のCOMインタフェースを自作するので、そのCOMインタフェースに唯一対応するIIDを定義する必要があります。

次のコードのように、IFileOpenDialog COMインタフェースが実装されたCOMオブジェクトを指定するためにCLSID_FileOpenDialogが用意されていました。 また、IFileOpenDialog COMインタフェースの親の親のIModalWindow COMインタフェースに対応するIID_IModalWindowが用意されていました。

extern(C) extern const CLSID CLSID_FileOpenDialog;

extern(C) extern const IID IID_IModalWindow;

// uuid("b4db1657-70d7-485e-8e3e-6fcb5a5c1802")
interface IModalWindow : IUnknown
{
extern (Windows):
    HRESULT Show(HWND hwndOwner);
}

CLSIDとIIDは外部宣言されていて実体uuidやguidを表す文字列はコード上に出てきませんが、 コメントアウトしておいた部分がIIDに対応するuuidです。

新しいuuidを生成する方法はいくつもありますが、Windows SDKに同梱されているuuidgen.exeを使ってみます。

"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\uuidgen.exe"

2回実行して2つのuuidを得ました。

PS C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64> ./uuidgen
f3488441-b094-430e-b34a-c18a5088b01e

PS C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64> ./uuidgen
0b425ee1-5991-4dbd-89e7-a12bf13fbaef

uuidをguidに変換する

実は、CLSIDとIIDは、GUID構造体のalias名です。 core.sys.windows.basetypsモジュールで次のとおり定義されています。

align(1) struct GUID {  // size is 16
    align(1):
    DWORD   Data1;
    WORD    Data2;
    WORD    Data3;
    BYTE[8] Data4;
}
alias GUID UUID, /*IID, CLSID, */FMTID, uuid_t;
alias IID = const(GUID);
alias CLSID = const(GUID);

したがって、本当に欲しいのはuuidではなくguidなので、uuidをguidに変換します。 何回も使うので変換関数を作っておきます。

GUID guidFromUUID(string uuidString)
{
    static import std.uuid;
    std.uuid.UUID uuid = std.uuid.UUID(uuidString);
    ubyte[8] data = uuid.data[8 .. $];
    return GUID(uuid.data[0] << 24 | uuid.data[1] << 16 | uuid.data[2] << 8 | uuid.data[3], uuid.data[4] << 8 | uuid.data[5], uuid.data[6] << 8 | uuid.data[7], data);
}

これでIID_IFilePickerとCLSID_FilePickerを定義できます。 この記事のコードでは、extern(C)はなくても動くのですが、一応付けておきます。

extern(C) const IID IID_IFilePicker = guidFromUUID("f3488441-b094-430e-b34a-c18a5088b01e");
extern(C) const CLSID CLSID_FilePicker = guidFromUUID("0b425ee1-5991-4dbd-89e7-a12bf13fbaef");

ここまでのまとめ

ここまでで、IFilePicker COMインタフェースとFilePicker COMオブジェクトのコードをまとめます。

extern(C) const IID IID_IFilePicker = guidFromUUID("f3488441-b094-430e-b34a-c18a5088b01e");

// uuid("f3488441-b094-430e-b34a-c18a5088b01e");
interface IFilePicker : IUnknown
{
extern (Windows):
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time);
}

extern(C) const CLSID CLSID_FilePicker = guidFromUUID("0b425ee1-5991-4dbd-89e7-a12bf13fbaef");

// uuid("0b425ee1-5991-4dbd-89e7-a12bf13fbaef");
class FilePicker : ComObject, IFilePicker
{
extern (Windows):
    override HRESULT QueryInterface(const(IID)* riid, void** ppv)
    {
        // ...
        return S_OK;
    }
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time)
    {
        // ...
        return S_OK;
    }
}

FilePickerクラスの実装は次の記事に回します。