しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

COM Interop の基本を IShellLink を使って学びなおした

WinQuickLook で COM インターフェース定義を大量に手書きした時、自分の COM Interop の知識がイマイチなことに気が付いたので、とても分かりやすい IShellLink を使ってまとめます。

ちなみに IShellLink はショートカットを作成するためのインターフェースです。

インターフェース定義を用意する

pinvoke.net には IShellLink の定義が存在しますが、頼っていると存在しないインターフェースへの対応が出来なくなるので、今回はヘッダーファイルから手動で起こします。

IShellLinkA (shobjidl_core.h) - Win32 apps | Microsoft Learn

実際のヘッダーファイルの定義は MIDL から自動生成されているので、コメントに MIDL での情報がメタデータ的に残っています。定義を引っ張ってきました。

MIDL_INTERFACE で設定されている GUID は IID になります。メソッドの定義順はそのまま vtable の順番になるので、絶対に変更してはいけません。[in][out] はそのまま C# の [In, Out] という属性に読み替えます。

LPWSTR は string か StringBuilder に、ポインタ系は IntPtr に読み替えていきます。実際に全てを C# で表現すると以下のようになります。

[ComImport]
[Guid("000214F9-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellLink
{
    void GetPath([Out] StringBuilder pszFile, int cch, [In, Out] IntPtr pfd, int fFlags);
    void GetIDList([Out] IntPtr ppidl);
    void SetIDList([In] IntPtr pidl);
    void GetDescription([Out] StringBuilder pszName, int cch);
    void SetDescription([In] string pszName);
    void GetWorkingDirectory([Out] StringBuilder pszDir, int cch);
    void SetWorkingDirectory([In] string pszDir);
    void GetArguments([Out] StringBuilder pszArgs, int cch);
    void SetArguments([In] string pszArgs);
    void GetHotkey([Out] out ushort pwHotkey);
    void SetHotkey([In] ushort wHotkey);
    void GetShowCmd([Out] out int piShowCmd);
    void SetShowCmd(int iShowCmd);
    void GetIconLocation([Out] StringBuilder pszIconPath, int cch, [Out] out int piIcon);
    void SetIconLocation([In] string pszIconPath, int iIcon);
    void SetRelativePath([In] string pszPathRel, int dwReserved);
    void Resolve([In] IntPtr hwnd, int fFlags);
    void SetPath([In] string pszFile);
}

インターフェースを全て定義したとしても、実際には使うメソッドはごくわずかだったりします。例えば今回はショートカットを作成したいだけなので、必要な IShellLink のメソッドは SetPath だけになります。

あまりにも面倒なので CLR には _VtblGap から始まる特殊なメソッドを定義することで、指定した数だけ vtable をスキップさせることが出来るようになっています。

[ComImport]
[Guid("000214F9-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellLink
{
    void _VtblGap1_17();
    void SetPath([In] string pszFile);
}

_VtblGap1_17 という名前は 17 個のメソッドをスキップさせるという意味になります。

簡略化したインターフェース定義はとてもすっきりしますが、後からのメソッド追加などが面倒になるので、定義が多すぎる場合などに限って使う方が良いと思います。

インスタンスを作成する

ネイティブだと CoCreateInstance を使ってインスタンスを作成します。IShellLink を作成するための CLSID と IID を指定すると、IShellLink のインスタンスが返ってきます。

IShellLink* pShellLink;

CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&pShellLink);

C# でインスタンスを作る方法としては CoClass を定義する方法がありますが、個人的には Type を作成して Activator で作成する方法がネイティブに近いので好きです。

public static class CLSID
{
    public static readonly Guid ShellLink = new Guid("00021401-0000-0000-C000-000000000046");

    public static readonly Type ShellLinkType = Type.GetTypeFromCLSID(ShellLink);
}

// CoCreateInstance ではなく Activator で RCW を作成
var shellLink = (IShellLink)Activator.CreateInstance(CLSID.ShellLinkType);

CLSID と CLSID から作成した Type は静的クラスとして定義しておけば扱いやすいです。

別のインターフェースを取得する

ショートカットを作成するためには、IShellLink から IPersistFile を取得する必要がありますが、COM では QueryInterface を呼び出して取得するのが基本です。

IPersistFile* pPersistFile;

pShellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&pPersistFile);

RCW では QueryInterface は用意されていないので、キャストを行って取得します。

var persistFile = (IPersistFile)shellLink;

IPersistFile は BCL に予め定義されているものがあるので、そっちを使うことにします。

インスタンスを開放する

COM では使い終わったインスタンスに対して Release メソッドを呼び出して、参照カウントを減らします。

pPersistFile->Release();
pShellLink->Release();

RCW では IUnknown が隠されているので、代わりに Marshal.ReleaseComObject を使って RCW の参照カウントを減らします。ちなみに RCW の参照カウントと COM の参照カウントは別物です。

Marshal.ReleaseComObject(pShellLink);

IPersistFile に対しては ReleaseComObject を呼び出す必要はありません。RCW を使っている限りキャストで取得したオブジェクトは自動で開放されます。

これが基本的な COM を使った処理の流れになります。最後にそれっぽくまとめます。

  • _VtblGap メソッドを使うと vtable の定義をスキップ出来る
  • CLSID を使って RCW インスタンスを作成する
  • COM と RCW の参照カウントは別物
  • RCW に対するキャストで取得したインスタンスは ReleaseComObject が不要

IShellLink はとても簡単なインターフェースですが、複雑なインターフェースでも基本は変わりません。もし定義が間違っている場合には、CLR が比較的わかりやすい例外を返してくれます。