...ing logging 4.0

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

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

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

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

その7ではCOMオブジェクトとCOMインタフェースを自作するための準備をしました。 その8とその9では自作COMファクトリをCOMサーバーとして登録し、 自作COMオブジェクトと自作COMインタフェースを作成できました。 その10では既存のCOMインタフェースを拡張した新しいCOMインタフェースとCOMオブジェクトを作成しました。 その11では、COMファクトリクラスを拡張して、新しいCOMインタフェースを使えました。

その12では、AddRefメソッドとReleaseメソッドを自分で呼ばなくていいように ComPtrというスマートポインタを作ることにしましたが、 その13では構築と破棄を同じ回数捉えることができませんでした。 その14ではコピーコンストラクタだけでスマートポインタを作る実験をしました。 その15ではComPtrを実際に使うことができました。

ComPtrでQueryInterfaceを使う

ComPtrがひとまず使えるようになりましたが、もう少し機能を追加してみます。

あるCOMオブジェクトのCOMインタフェースを取得済みの状況で、 そのCOMオブジェクトの別のCOMインタフェースをQueryIntefaceで取得したくなったと仮定します。

例えば、最初に作ったIFilePicker COMインタフェースを取得済みの状況で、 IFilePicker2 COMインタフェースを取得したくなったとしましょう。 実際には機能が同じなのであまり意味はないですが。

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

    ComPtr!IFilePicker2 pPicker = createFilePicker();

    PickedFile file;
    file.path = cast(wchar*)new wchar[MAX_PATH];
    hr = pPicker.pickFile(&file);
    if (FAILED(hr)) return;

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

    { // ★QueryInterfaceでIFilePicker COMインタフェースを取得
        LPWSTR path = cast(wchar*)new wchar[MAX_PATH];
        SYSTEMTIME time;
        ComPtr!IFilePicker pPicker1;
        pPicker.QueryInterface(&IID_IFilePicker, cast(void**)&pPicker1);
        pPicker1.pickFile(&path, &time);

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

既にIFilePicker2 COMインタフェースをpPickerに保持しているので、 pPickerのQueryInterfaceメソッドを使って、IID_FilePickerに対応するCOMインタフェースである IFilePicker COMインタフェースを取得しました。

これで実行すると、追加した2回目がちゃんと動いています。

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

ComPtrでQueryInterfaceを隠ぺいする

ComPtr!IFilePicker pPicker1;
pPicker.QueryInterface(&IID_IFilePicker, cast(void**)&pPicker1);
pPicker1.pickFile(&path, &time);

これでも動いているのですが、初期化されていないComPtrは 何のCOMオブジェクトも指していない状態になっているので、 できれば次のように初期化してしまいたい。

ComPtr!IFilePicker pPicker1 = pPicker.SOMETHING_METHOD(&IID_IFilePicker);
pPicker1.pickFile(&path, &time);

QueryInterfaceが受け取らないといけない情報は、次のふたつでした。

  • 何のCOMインタフェースが欲しいか(IID_...)
  • それが何のクラスに対応するか(TargetType)

現状では、IID_IFilePickerの指定は必要です。 また、IIDに対応するクラス(TargetType)を指定するにはテンプレートを使えます。 そうすると、SOMETHING_METHODは、次のような感じになります。

struct ComPtr(BaseType : IUnknown) {
    // ...
    ComPtr!TargetType as(TargetType)(IID* riid) // ★追加
    {
        // ...

asメソッドの中身を実装すると、次のようになりました。

なお、asの戻り値はComPtrになるので、HRESULTを返す代わりに例外を投げることにしました。

struct ComPtr(BaseType : IUnknown)
{
    this(BaseType comPtr)
    {
        _comObj = comPtr;
    }
    this(ref ComPtr!BaseType comPtr)
    {
        _comObj = comPtr._comObj;
        _comObj.AddRef();
    }
    ~this()
    {
        if (!_comObj) return;
        _comObj.Release();
        _comObj = null;
    }
    ComPtr!TargetType as(TargetType)(IID* riid) // ★追加
    {
        TargetType target;
        HRESULT hr = _comObj.QueryInterface(riid, cast(void**)&target);
        if (hr < 0)
            throw new ComException("ComPtr.as is failed.");
        return ComPtr!TargetType(target);
    }
    @property BaseType handle()
    {
        return _comObj;
    }
    alias handle this;
private:
    BaseType _comObj;
}

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

    ComPtr!IFilePicker2 pPicker = createFilePicker();

    PickedFile file;
    file.path = cast(wchar*)new wchar[MAX_PATH];
    hr = pPicker.pickFile(&file);
    if (FAILED(hr)) return;

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

    { // ★ComPtr.asでIFilePicker COMインタフェースを取得
        LPWSTR path = cast(wchar*)new wchar[MAX_PATH];
        SYSTEMTIME time;
        // ComPtr!IFilePicker pPicker1;
        // pPicker.QueryInterface(&IID_IFilePicker, cast(void**)&pPicker1);
        ComPtr!IFilePicker pPicker1 = pPicker.as!IFilePicker(&IID_IFilePicker); // ★書換
        pPicker1.pickFile(&path, &time);

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

実行してみるとちゃんと動きました。

C:\d\gitproj\dfl\examples\toggleswitch\shell.bat
2025-10-16
C:\d\gitproj\dfl\examples\toggleswitch\README.md
2025-10-16

これでQueryInterfaceを隠ぺいすることができました。

最後に

実行可能なソースコード全文です。

なお、ComPtrにはさらに管理対象オブジェクトのアドレスを取得するための ptrメソッドを追加してありますが、説明は省略します。

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

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

extern(C) const IID IID_IFilePicker2 = guidFromUUID("98afd9b2-cb71-41db-9155-c689512c6531");

// uuid("98afd9b2-cb71-41db-9155-c689512c6531");
interface IFilePicker2 : IUnknown
{
extern (Windows):
    HRESULT pickFile(PickedFile* file);
}

struct PickedFile
{
    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 || *riid == IID_IFilePicker2)
        {
            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, IFilePicker2
{
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 if (*riid == IID_IFilePicker2)
        {
            *ppv = cast(void*)cast(IFilePicker2)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;
    }
    HRESULT pickFile(PickedFile* file)
    {
        return pickFile(&file.path, &file.time);
    }
}

class ComException : Exception
{
    this(string msg, string file = __FILE__, int line = __LINE__)
    {
        super(msg, file, line);
    }
}

ComPtr!IFilePicker2 createFilePicker()
{
    HRESULT hr;

    ComPtr!FilePickerFactory pFactory;
    hr = CoGetClassObject(&CLSID_FilePicker, CLSCTX_INPROC_SERVER, null, &IID_IClassFactory, cast(void**)&pFactory);
    if (FAILED(hr)) throw new ComException("CoGetClassObject failure.");

    ComPtr!IFilePicker2 pPicker;
    hr = pFactory.CreateInstance(null, &IID_IFilePicker2, cast(void**)&pPicker);
    if (FAILED(hr)) throw new ComException("CreateInstance failure.");

    return pPicker;
}

struct ComPtr(BaseType : IUnknown)
{
    this(BaseType comPtr)
    {
        _comObj = comPtr;
    }
    this(ref ComPtr!BaseType comPtr)
    {
        _comObj = comPtr._comObj;
        _comObj.AddRef();
    }
    ~this()
    {
        if (!_comObj) return;
        _comObj.Release();
        _comObj = null;
    }
    ComPtr!TargetType as(TargetType)(IID* riid)
    {
        TargetType target;
        HRESULT hr = _comObj.QueryInterface(riid, cast(void**)&target);
        if (hr < 0)
            throw new ComException("ComPtr.as is failed.");
        return ComPtr!TargetType(target);
    }
    @property BaseType handle()
    {
        return _comObj;
    }
    @property BaseType* ptr()
    {
        return &_comObj;
    }
    alias handle this;
private:
    BaseType _comObj;
}

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

    ComPtr!IFilePicker2 pPicker = createFilePicker();

    PickedFile file;
    file.path = cast(wchar*)new wchar[MAX_PATH];
    hr = pPicker.pickFile(&file);
    if (FAILED(hr)) return;

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

    { // QueryInterfaceでIFilePicker COMインタフェースを取得
        LPWSTR path = cast(wchar*)new wchar[MAX_PATH];
        SYSTEMTIME time;
        // ComPtr!IFilePicker pPicker1;
        // pPicker.QueryInterface(&IID_IFilePicker, cast(void**)&pPicker1);
        ComPtr!IFilePicker pPicker1 = pPicker.as!IFilePicker(&IID_IFilePicker);
        pPicker1.pickFile(&path, &time);

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