...ing logging 4.0

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

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

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

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

COMインタフェースの継承関係があればダウンキャストできるか

その5の記事で、

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

というコードがありました。

IShellItem COMインタフェースからIShellItem2 COMインタフェースを取得するために、 QueryInterfaceメソッドを使いました。

でも、ちょっと待てよ。 IShellItemとIShellItem2の宣言を見てみると、IShellItem2インタフェースがIShellItemインタフェースを継承しているから、 QueryInterfaceを使わなくてもダウンキャストできるんじゃないか。

interface IShellItem : IUnknown
interface IShellItem2 : IShellItem

つまり、次のとおりではなくて、

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

単に次のとおり書けないか。

IShellItem2 pItem2 = cast(IShellItem2)pItem;

COMインタフェースはアップキャストもダウンキャストもできない

答えは公式サイトにあります。

Interfaces - D Programming Language

References cannot be upcast to the enclosing class object, nor can they be downcast to a derived interface. Implement QueryInterface() for that interface in standard COM fashion to convert to another COM interface.

邦訳サイトを見てみます。

インターフェイス - プログラミング言語 D (日本語訳)

COM インターフェイス

インターフェイスの一種として、COMインターフェイスがあります。COMインターフェイスは、 WindowsのCOMオブジェクトとして直接適合するように設計されます。全ての COM オブジェクトは COMインターフェイスによって表現でき、COMインターフェイスを持つ全ての D言語のオブジェクトは、外部のCOMクライアントから使用できます。

COMインターフェイスは、std.c.windows.com.IUnknown から派生することで定義します。COMインターフェイスは、 D言語の通常のインターフェイスと以下の点で異なります:

  • std.c.windows.com.IUnknown から派生している。
  • DeleteExpression の引数として使えない。
  • 参照は、周囲のクラスのオブジェクトへのUpcastや、 派生インターフェイスへのDowncastをすることが許されない。 この目的には、COMの標準的なやり方で適切な QueryInterface() が実装されていなければなりません。
  • COMインターフェイスから派生したクラスはCOMクラスです。
  • COMクラスのメンバ関数のデフォルトのリンケージは is extern(System) です
  • vtbl[] の先頭のメンバは InterfaceInfo へのポインタではなく、最初の仮想関数ポインタになります。

さらに詳しい情報については Modern COM Programming in D をご覧下さい。

はい、許されない。

でもやってみる

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 = cast(IShellItem2)pItem; // やってはいけない
    // 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);
}

コンパイルはできたので実行します。

PS C:\d\gitproj\dfl\examples\text2> dub
    Starting Performing "debug" build using C:\D\dmd2\windows\bin64\dmd.exe for x86_64.
  Up-to-date dfl 0.13.1+commit.1.gff6b658: target for configuration [library] is up to date.
    Building text ~master: building configuration [application]
     Linking text
    Finished To force a rebuild of up-to-date targets, run again with --force
     Running bin/text.exe 
C:\d\gitproj\dfl\examples\toggleswitch\toggleswitch.code-workspace
2025-10-16
PS C:\d\gitproj\dfl\examples\text2> 

何事もなく動きました。 でも、たまたま動いたと受け止めるのが正しいのだと思います。

QueryInterfaceメソッドを使えば、戻り値のHRESULTを見て取得に成功したかどうか分かりますが、 ダウンキャストだったらどうなるか未知数です。

公式に禁止されているので、やらないようにしましょう。