ITEMIDLISTとCIDAまとめ

ITEMIDLIST(PIDL)やCIDAについて何度調べても忘れるので、記事にまとめます。

構造体定義

登場する構造体の定義と関連ページを列挙しておきます。

整数型のサイズ

typedef struct _IDA {
  UINT cidl;
  UINT aoffset[1];
} CIDA, *LPIDA;

typedef struct _ITEMIDLIST {
  SHITEMID mkid;
} ITEMIDLIST;

typedef struct _SHITEMID {
  USHORT cb;
  BYTE   abID[1];
} SHITEMID;

アイテムIDリスト

アイテムIDリスト(ITEMIDLIST)は、シェル名前空間におけるPathのようなものです。ディレクトリと異なり、デスクトップがルートとなるツリーになっています。少しややこしいので順を追って説明します。

アイテムID(SHITEMID)
要するに、「C:\」といったフォルダ名やファイル名にあたる構造体です。abID[1]となっていることからわかるように、cbというサイズの可変長構造体です。しかし、この構造体をプログラマが直接使うことはありません。 また、abIDの意味は親フォルダのみが知っており、プログラマが中身のバイナリの意味を解釈することはありません。
アイテムIDリスト(ITEMIDLIST)
要するに、Pathです。c:\sample.txtというパスは、シェル名前空間上では「デスクトップ」→「C:\」→「sample.txt」→「\0\0」という3つのSHITEMID+終端を連結したリスト(ITEMIDLIST)で表します。 SHITEMIDを直接使うことはないと言いましたが、文字列と同様にこのNULL終端が常に伴うので、「単体のアイテムID」を使うことが通常ないためです。
PIDL
アイテムIDリストへのポインタ(LPITEMIDLIST)です。可変長構造体を値として扱うのは困難なので、事実上常にポインタとして扱います。頻出するためPIDLという略称が与えられています。

図解

ITEMIDLIST

さらに詳細な話

途中のWindowsから型がより厳密となり、以下のような種類に別れました。

IDLIST_ABSOLUTE
要するに絶対パスです。ルートからすべてのITEMIDが並んでいます
IDLIST_RELATIVE
要するに相対パスです。親からの相対的なITEMIDが並んでいます。
ITEMID_CHILD
NULL終端のないSHITEMIDです。ということは、構文的にも意味論的にもSHITEMIDのエイリアスです。ITEMIDLISTを加工する途中とかで現れたりします。

また、上記3つにP、C、Uの組み合わせのPrefixをつけることでポインタ型を表します。

P (Pointer)
ポインタの意味です。(念の為ですが、Win32ではLPとPに違いはありません。)
C (Constant)
const指定のついたポインタです
U (unaligned)
ポインタがCPUアーキテクチャごとの境界に沿ってない可能性があることを表します。ITEMIDLISTの途中にあるSHITEMIDを指しているときが主でしょう。…なぜalignedが求められるのか、私は理解していません。Itaniumでや組み込み系では__unalignedを指定しないといけないアーキテクチャが存在するからでしょうか。

CIDA

CFSTR_SHELLIDLISTで使われる、複数のITEMIDLISTを表す構造体です。要するにITEMIDLISTのリストです。 (CIDAはClipboard itemIDlist Arrayの略とかでしょうか…)

概念図

構造体は上に示しましたが、これでは意味不明なので概念を表します。ITEMIDLISTはそもそも可変長構造体なので、そのままC言語で記述する方法はありません。

typedef struct _IDA概念 {
  UINT cidl;
  UINT aoffset[cidl+1];
  IDLIST_ABSOLUTE base;
  IDLIST_RELATIVE children[cidl];
} CIDA概念, *LPIDA概念;

図で表すとこうなります。

CIDA

説明

cidl
いくつのアイテムを表しているか
aoffset
それぞれのアイテムの、この構造体の頭からのオフセットを表します。parentへのオフセットも含まれていることに注意。
base
後ろのchildrenの起点となるPIDLです。「If this PIDL is empty」とありますが、そもそもEmptyなPIDLってなんだ。
children
本命のITEMIDLISTです

読み取る際には、parentにchildrenをつなげることで、絶対PIDLを求めることができます。逆に、CIDAを構築する場合、Win32ならCIDLData_CreateFromIDArraySHCreateDataObjectを使うことになります。

起点となるPIDLを指定できるのですが、この機能の必要性はあまりなさそうな気がしています。普通にデスクトップを起点指定して、childrenには絶対PIDLを入れればいいと思うんですが。

C#で読む

C#で頑張って読みます。PIDLは以下のパッケージを使っています。 Vanara/ShTypes.PIDL.cs at a75f2dd17d8df89f4bb6132c8978d34bc116a675 · dahall/Vanara

// 現在作成中のプログラムから抜き出したので、コンパイル通らなければごめん
unsafe void (List<Shell32.PIDL>, Shell32.PIDL) ReadCida( void *p )
{
  var l = new List<Shell32.PIDL>();
  Shell32.PIDL? parent = null;
  int cb = Unsafe.Read<int>(p);                     
  ReadOnlySpan<uint> x = new ReadOnlySpan<uint>(p,cb+2);
  byte* ptr = (byte*)p;
  parent = new Shell32.PIDL((IntPtr)(ptr + x[1]), true, true);
  for (var i = 1; i < x[0] + 1; i++)
  {
      // childはpの一部なので、開放してはならない
      var child = new Shell32.PIDL((IntPtr)(ptr + x[i + 1]), false, false);
      var pa = new Shell32.PIDL(parent);
      pa.Append(child);
      l.Add(pa);
  }
  return (l, parent);
}

C#で書く

CIDAを頑張ってがりがり作ります。若干自信がないですが、CIDLData_CreateFromIDArrayを呼び出すよりはC#的な相性はいいかなぁと思ってます。

unsafe void WriteCida(PIDL[] pidls, PIDL? parent = null)
{
  if (parent == null)
  {
      SHGetKnownFolderIDList(KNOWNFOLDERID.FOLDERID_Desktop.Guid(), 0, HTOKEN.NULL, out parent);
  }
  int size = 4 + 4 + pidls.Length * 4; // cidl + aoffset[0] + aoffset(子ノード分)
  Span<byte> span = stackalloc byte[size + ((int)parent.Size + (int)pidls.Sum(x => x.Size))];
  Span<uint> uspan = MemoryMarshal.Cast<byte,uint>(span);
  uspan[0] = (uint)pidls.Length;
  uspan[1] = (uint)size;
  uspan[2] = (uint)size + parent.Size;
  unsafe
  {
      fixed (byte* ptr = span)
      {
          Buffer.MemoryCopy((void*)parent.DangerousGetHandle(), ptr + size, parent.Size, parent.Size);
          for (int i = 0; i < pidls.Length; i++)
          {
              Buffer.MemoryCopy((void*)pidls[i].DangerousGetHandle(), ptr + uspan[2 + i], pidls[i].Size, pidls[i].Size);
              if (i != pidls.Length - 1) uspan[3 + i] = uspan[2 + i] + pidls[i].Size;
          }
      }
  }

  // Do something with span
}

まとめ

PIDLは破綻はしてないものの、なかなか複雑で厳しいです。OSの世代が新しくなるにつれて使いやすいAPIが追加されており、いくらかマシかも知れません。

他の言語から呼び出す場合、変なAPIを呼び出すよりメモリ上のデータを直接操作するほうが楽でオーバーヘッドやメモリ解放の手間がないという話もあったりします。

参考文献