...ing logging 4.0

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

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

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

その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ではコピーコンストラクタだけでスマートポインタを作る実験をしました。

構造体テンプレートComPtrの基本形を書く

前回の記事でやったことを踏まえると、構造テンプレートComPtrをこのように書けばよさそうです。 スマートポインタで管理するCOMクラスをBaseTypeと呼ぶことにして、 そのBaseTypeはIUnknownクラスとその子クラスだけ受け入れるようにしています。 後は前回やったことと同じです。

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

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

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

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

Releaseメソッドを自分で呼んでいるところが2か所あるので、それぞれでComPtrを使ってみます。

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;
    ComPtr!FilePickerFactory pFactory;
    hr = CoGetClassObject(&CLSID_FilePicker, CLSCTX_INPROC_SERVER, null, &IID_IClassFactory, cast(void**)&pFactory);
    if (FAILED(hr)) throw new ComException("CoGetClassObject failure.");
    // scope(exit) pFactory.Release();

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

    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そのものはComPtrに格納されたオブジェクトではないので、 次の2行のようには書けません。

hr = pFactory.CreateInstance(null, &IID_IFilePicker2, cast(void**)&pPicker);
hr = pPicker.pickFile(&file);

格納されたオブジェクトに直接アクセスしたくなることもあるので、 それにアクセスできるhandleメソッドを追加します。

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;
    }
    @property BaseType handle() // ★追加
    {
        return _comObj;
    }
private:
    BaseType _comObj;
}

こうしてhandleメソッドを呼んで中身を取り出せば、これまでどおり動きます。

ところで

ComPtrのアドレスを取得してvoid**にキャストしていますけど何事もなく動いています。 何が起こっているんでしょう?

hr = pFactory.handle.CreateInstance(null, &IID_IFilePicker2, cast(void**)&pPicker);
hr = pPicker.handle.pickFile(&file);

一応動いたけれども

handleメソッドを自分で呼び出せば動くようにはなりましたが、 それは面倒なので、alias thisを使いましょう。

先ほど作ったhandleメソッドは中身のオブジェクトを返すので、 ComPtrに存在しないメソッドを呼び出そうとしたら、 handleメソッドを介して中身のオブジェクトに全ての呼び出しを転送してしまいます。 だいぶ悪いことができそうなテクニックです。

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;
    }
    @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!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.");

    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に格納されていることをあまり意識せずによくなり、 COMオブジェクトに対して自分でAddRef/Releaseを呼ばなくてもよくなりました。

COMインタフェースの取得を関数に切り出す

ComPtrを作ったので、関数の引数や戻り値でCOMインタフェースを受け渡しするのも簡単になりました。 関数内でCOMインタフェースを取得して、返す関数を作ってみます。

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

これでファクトリCOMオブジェクトFilePickerFactoryが隠ぺいされて main関数の見通しが少しだけ良くなりました。

最後に

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

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

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