...ing logging 4.0

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

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

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

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

その7ではCOMオブジェクトとCOMインタフェースを自作するための準備をして、その8で完成させました。

しかしながら、その8のコードでは、 CoCreateInstance関数がCOMオブジェクトを返してくれませんでした。 なぜなら、Windowsはユーザーが勝手に作ったCOMオブジェクトのCLSIDを知らないからです。

WindowsにCOMオブジェクトのCLSIDを登録する方法

というわけで、WindowsにCOMオブジェクトのCLSIDを登録してあげないといけません。

これには大きく分けてふたつの方法があります。

一つ目は、レジストリにCLSIDを登録する方法です。こちらの方が一般的な方法です。 Windowsの様々な機能がCOMオブジェクトとして公開されており、すべてCLSIDが割り当てられています。 レジストリに登録されているので、ユーザーは自作のプログラムからCOMオブジェクトを使うことができます。

二つ目は、実行中にCoRegisterClassObject関数を呼び出して、 CLSIDとそのCLSIDに対応するCOMオブジェクトを生成するCOMサーバーを登録する方法です。 使い終わったら、登録を削除するために、CoRevokeClassObject関数を呼び出します。

CoRegisterClassObject 関数 (combaseapi.h) - Win32 apps | Microsoft Learn

CoRevokeClassObject 関数 (combaseapi.h) - Win32 apps | Microsoft Learn

レジストリをいじるのは危険が伴うので、この記事では二つ目の方法を採用します。

COMサーバーを作る

CoCreateInstance関数を呼び出すと、なんやかんやあってCOMサーバーがCOMオブジェクトを生成してくれるようですが、 自作したCOMオブジェクトなので、COMオブジェクトを生成する処理も自分で書かないといけません。

COMオブジェクトを生成するためには、IClassFactory COMインタフェースを実装したファクトリCOMオブジェクトを作成する必要があります。

IClassFactory - Win32 apps | Microsoft Learn

core.sys.windows.unknwnモジュールのIClassFactoryの定義は次のとおりです。

interface IClassFactory : IUnknown {
    HRESULT CreateInstance(IUnknown UnkOuter, IID* riid, void** pvObject);
    HRESULT LockServer(BOOL fLock);
}

このCOMインタフェースを実装したFilePickerFactory COMオブジェクトを作ります。

例によって、基底クラスをComObjectクラスにしてAddRefとReleaseの実装を省略し、 IClassFactory COMインタフェースを実装します。

class FilePickerFactory : ComObject, IClassFactory
{
extern(Windows):
    override HRESULT QueryInterface(const(IID)* riid, void** ppv)
    {
        if (*riid == IID_IClassFactory || *riid == IID_IUnknown)
        {
            *ppv = cast(void*)this;
            AddRef();
            return S_OK;
        }
        else
        {
            *ppv = null;
            return E_NOINTERFACE;
        }
    }
    HRESULT CreateInstance(IUnknown pUnkOuter, const(IID)* riid, void** ppv)
    {
        if (pUnkOuter !is null) return CLASS_E_NOAGGREGATION;
        if (*riid == IID_IFilePicker)
        {
            auto obj = new FilePicker;
            return obj.QueryInterface(riid, ppv);
        }
        *ppv = null;
        return E_NOINTERFACE;
    }
    HRESULT LockServer(BOOL)
    {
        return S_OK;
    }
}

ほとんど定型文ですね。

QueryInterfaceメソッドは、FilePicker COMオブジェクトで書いたものとさほど変わりません。

CreateInstanceメソッドでは、FilePickerFactoryが生成するCOMインタフェースのIIDはIID_IFilePickerだけなので、 riidがIID_IFilePickerかどうか判定して、一致していたらFilePicker COMオブジェクトを作成します。 作成したCOMオブジェクトをppvに代入すればいいですが、ここではQueryInterfaceメソッドを呼んでおきます。

もしriidに対応していないIIDが指定された場合は、ppvにnullを代入して、E_NOINTERFACEエラーコードを返します。

COMサーバーをWindowsに登録する

ファクトリCOMオブジェクトができたので、WindowsにCOMサーバーとして登録します。

DWORD cookie;
auto factory = new FilePickerFactory();

hr = CoRegisterClassObject(&CLSID_FilePicker, factory, CLSCTX_INPROC_SERVER, REGCLS.REGCLS_MULTIPLEUSE, &cookie);
if (FAILED(hr)) return;

CoRegisterClassObject関数の第1引数は、登録したいCLSIDを指定します。今回はCLSID_FilePickerです。

第2引数は、ファクトリCOMオブジェクトであるFilePickerFactoryのインスタンスを指定します。

第3引数は、ここではCOMサーバーとそれを呼び出す側(COMクライアント)が同じプロセスで 実行されたらいいので、CLSCTX_INPROC_SERVERにします。

CLSCTX (wtypesbase.h) - Win32 apps | Microsoft Learn

第4引数は、REGCLS.REGCLS_MULTIPLEUSEにしておきます。

REGCLS (combaseapi.h) - Win32 apps | Microsoft Learn

第5引数は、登録を解除するときに使う番号が返されるので、それをcookie変数で受け取ります。

COMサーバーの登録を解除する

登録したCOMサーバーを使い終わったら、cookie変数に保存しておいた番号を CoRevokeClassObject関数に与えて、登録を解除します。

scope(exit) CoRevokeClassObject(cookie);

登録したCOMサーバーからCOMファクトリオブジェクトを取得する

それではCoGetClassObject関数を使って、COMファクトリオブジェクトを取得します。

FilePickerFactory pFactory;
hr = CoGetClassObject(&CLSID_FilePicker, CLSCTX_INPROC_SERVER, null, &IID_IClassFactory, cast(void**)&pFactory);
if (FAILED(hr)) return;
scope(exit) pFactory.Release();

CoGetClassObject関数の第1引数では、取得したいCOMオブジェクトのCLSID_FilePickerを指定します。

第2引数では、CLSCTX_INPROC_SERVERを、第3引数では、nullを指定します。

第4引数では、IID_IClassFactoryを指定します。ここはいつでもIID_IClassFactoryのようです。

第5引数では、FilePickerFactoryのインスタンスを受け取る変数のアドレスを指定します。 ここにはcast(void**)が必要です。

これでCOMファクトリオブジェクトをpFactory変数に取得できました。

COMファクトリオブジェクトにCOMオブジェクトを作成してもらう

続いて、pPicker変数に受け取ったCOMファクトリオブジェクトに対してCreateInstanceメソッドを呼び出して、 IFilePicker COMインタフェースを介してアクセスできるCOMオブジェクトを取得します。

IFilePicker pPicker;
hr = pFactory.CreateInstance(null, &IID_IFilePicker, cast(void**)&pPicker);
if (FAILED(hr)) return;
scope(exit) pPicker.Release();

これでやっとpPickerにIFilePicker COMインタフェースを取得できました。

実行してみる

以下、ソース全文です。果たして今度は実行できるでしょうか。

import std.stdio;
import core.sys.windows.windef;
import core.sys.windows.com;
import core.sys.windows.objbase;
import core.sys.windows.winbase;

extern(C) extern const IID IID_IModalWindow;

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

extern(C) extern const IID IID_IFileDialog;

// uuid("42f85136-db7e-439c-85f1-e4075d135fc8")
interface IFileDialog : IModalWindow
{
extern (Windows):
    HRESULT SetFileTypes();//(UINT cFileTypes, const COMDLG_FILTERSPEC* rgFilterSpec);
    HRESULT SetFileTypeIndex();//(UINT iFileType);
    HRESULT GetFileTypeIndex();//(UINT* piFileType);
    HRESULT Advise();//(IFileDialogEvents pfde, DWORD* pdwCookie);
    HRESULT Unadvise();//(DWORD dwCookie);
    HRESULT SetOptions();//(FILEOPENDIALOGOPTIONS fos);
    HRESULT GetOptions();//(FILEOPENDIALOGOPTIONS * pfos);
    HRESULT SetDefaultFolder();//(IShellItem psi);
    HRESULT SetFolder();//(IShellItem psi);
    HRESULT GetFolder();//(IShellItem* ppsi);
    HRESULT GetCurrentSelection();//(IShellItem* ppsi);
    HRESULT SetFileName();//(LPCWSTR pszName);
    HRESULT GetFileName();//(LPWSTR pszName);
    HRESULT SetTitle();//(LPCWSTR pszTitle);
    HRESULT SetOkButtonLabel();//(LPCWSTR pszText);
    HRESULT SetFileNameLabel();//(LPCWSTR pszLabel);
    HRESULT GetResult(IShellItem* ppsi);
    HRESULT AddPlace();//(IShellItem psi, FDAP fdap);
    HRESULT SetDefaultExtension();//(LPCWSTR pszDefaultExtension);
    HRESULT Close();//(HRESULT hr);
    HRESULT SetClientGuid();//(REFGUID guid);
    HRESULT ClearClientData();
    HRESULT SetFilter();//(IShellItemFilter pFilter);
}

extern(C) extern const IID IID_IFileOpenDialog;

// uuid("d57c7288-d4ad-4768-be02-9d969532d960")
interface IFileOpenDialog : IFileDialog
{
extern(Windows):
    HRESULT GetResults();//(IShellItemArray* ppenum);
    HRESULT GetSelectedItems();//(IShellItemArray* ppsai);
}

extern(C) extern const IID IID_IShellItem;

// uuid("43826d1e-e718-42ee-bc55-a1e261c37bfe")
interface IShellItem : IUnknown
{
extern(Windows):
    HRESULT BindToHandler();//(IBindCtx pbc, REFGUID bhid, REFIID riid, void** ppv);
    HRESULT GetParent();//(IShellItem* ppsi);
    HRESULT GetDisplayName(SIGDN sigdnName, LPWSTR* ppszName);
    HRESULT GetAttributes();//(SFGAOF sfgaoMask, SFGAOF* psfgaoAttribs);
    HRESULT Compare();//(IShellItem psi, SICHINTF hint, int* piOrder);
}

enum SIGDN : int
{
    SIGDN_NORMALDISPLAY = 0,
    SIGDN_PARENTRELATIVEPARSING = 0x80018001,
    SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
    SIGDN_PARENTRELATIVEEDITING = 0x80031001,
    SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
    SIGDN_FILESYSPATH = 0x80058000,
    SIGDN_URL = 0x80068000,
    SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
    SIGDN_PARENTRELATIVE = 0x80080001,
    SIGDN_PARENTRELATIVEFORUI = 0x80094001
}

extern(C) extern const CLSID CLSID_FileOpenDialog;

extern(C) extern const IID IID_IShellItem2;

struct PROPERTYKEY
{
    GUID fmtid;
    DWORD pid;
}
alias REFPROPERTYKEY = PROPERTYKEY*;

// uuid("7e9fb0d3-919f-4307-ab2e-9b1860310c93")
interface IShellItem2 : IShellItem
{
extern(Windows):
    HRESULT GetPropertyStore();//(GETPROPERTYSTOREFLAGS flags, REFIID riid, void **ppv);
    HRESULT GetPropertyStoreWithCreateObject();//(GETPROPERTYSTOREFLAGS flags, IUnknown *punkCreateObject, REFIID riid, void **ppv);
    HRESULT GetPropertyStoreForKeys();//(const PROPERTYKEY *rgKeys, UINT cKeys, GETPROPERTYSTOREFLAGS flags, REFIID riid, void **ppv);
    HRESULT GetPropertyDescriptionList();//(REFPROPERTYKEY keyType, REFIID riid, void **ppv);
    HRESULT Update();//(IBindCtx *pbc);
    HRESULT GetProperty();//(REFPROPERTYKEY key, PROPVARIANT *ppropvar);
    HRESULT GetCLSID();//(REFPROPERTYKEY key, CLSID *pclsid);
    HRESULT GetFileTime(REFPROPERTYKEY key, FILETIME *pft);
    HRESULT GetInt32();//(REFPROPERTYKEY key, int *pi);
    HRESULT GetString();//(REFPROPERTYKEY key, LPWSTR *ppsz);
    HRESULT GetUInt32();//(REFPROPERTYKEY key, ULONG *pui);
    HRESULT GetUInt64();//(REFPROPERTYKEY key, ULONGLONG *pull);
    HRESULT GetBool();//(REFPROPERTYKEY key, BOOL *pf);
}

const PROPERTYKEY PKEY_DateCreated = {
    { 0xB725F130, 0x47EF, 0x101A, [0xA5, 0xF1, 0x02, 0x60, 0x8C, 0x9E, 0xEB, 0xAC ]}, 15
};

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);
}

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);
}

class FilePickerFactory : ComObject, IClassFactory
{
extern(Windows):
    override HRESULT QueryInterface(const(IID)* riid, void** ppv)
    {
        if (*riid == IID_IClassFactory || *riid == IID_IUnknown)
        {
            *ppv = cast(void*)this;
            AddRef();
            return S_OK;
        }
        else
        {
            *ppv = null;
            return E_NOINTERFACE;
        }
    }
    HRESULT CreateInstance(IUnknown pUnkOuter, const(IID)* riid, void** ppv)
    {
        if (pUnkOuter !is null) return CLASS_E_NOAGGREGATION;
        if (*riid == IID_IFilePicker)
        {
            auto obj = new FilePicker;
            return obj.QueryInterface(riid, ppv);
        }
        *ppv = null;
        return E_NOINTERFACE;
    }
    HRESULT LockServer(BOOL)
    {
        return S_OK;
    }
}

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)
    {
        if (*riid == IID_IFilePicker)
        {
            *ppv = cast(void*)cast(IFilePicker)this;
            AddRef();
            return S_OK;
        }
        else
        {
            return super.QueryInterface(riid, ppv);
        }
    }
    HRESULT pickFile(LPWSTR* path, SYSTEMTIME* time)
    {
        HRESULT hr;

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

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

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

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

        // import std.conv;
        // *path = outPath.to!string;
        import core.stdc.wchar_;
        wcscpy(*path, outPath);

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

        PROPERTYKEY prop = PKEY_DateCreated;
        FILETIME filetime;
        pItem2.GetFileTime(&prop, &filetime);
        FileTimeToSystemTime(&filetime, time);

        return S_OK;
    }
}

void main()
{
    HRESULT hr;

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

    DWORD cookie;
    auto factory = new FilePickerFactory();

    hr = CoRegisterClassObject(&CLSID_FilePicker, factory, CLSCTX_INPROC_SERVER, REGCLS.REGCLS_MULTIPLEUSE, &cookie);
    if (FAILED(hr)) return;
    scope(exit) CoRevokeClassObject(cookie);

    FilePickerFactory pFactory;
    hr = CoGetClassObject(&CLSID_FilePicker, CLSCTX_INPROC_SERVER, null, &IID_IClassFactory, cast(void**)&pFactory);
    if (FAILED(hr)) return;
    scope(exit) pFactory.Release();

    IFilePicker pPicker;
    hr = pFactory.CreateInstance(null, &IID_IFilePicker, cast(void**)&pPicker);
    if (FAILED(hr)) return;
    scope(exit) pPicker.Release();

    LPWSTR path = cast(wchar*)new wchar[MAX_PATH];
    SYSTEMTIME time;
    hr = pPicker.pickFile(&path, &time);
    if (FAILED(hr)) return;

    import std.conv;
    writeln(path.to!string);
    writefln("%s-%s-%s", time.wYear, time.wMonth, time.wDay);
}

今度はうまく動作しました。

C:\d\gitproj\dfl\examples\toggleswitch\dub.json
2025-10-16

これで、COMオブジェクトとCOMインタフェースを自作することができました。

でも、ファクトリクラスも実装しないといけなかったので、main関数はあまりシンプルにはなりませんでしたね。