Back to Writing

Jump List の仕組み

実 DLL の逆解析結果を元に、Jump List が内部でどの API を使って列挙・構築・表示されるのかを整理する。

March 15, 202612 min readWindows, Jump List, Shell

エクスプローラのジャンプリストは、とりわけ深い意味を持っている。サイドバーやクイックアクセスに表示される項目はエクスプローラーのジャンプリストの項目であり、クイックアクセスにアイテムをピン留めすることはジャンプリストのアイテムをピン留めすることと全く同じである。

ただ、内部実装は Windows Vista のジャンプリストの出現から今日の Windows 11 まで様々な実装は進化してきた。

この記事では、実 DLL の逆解析結果を元に、Jump List が内部でどの API を使って列挙・構築・表示されるのかを整理する。

TL;DR

公開 API だけを見ると、Jump List は ICustomDestinationListIApplicationDocumentLists、あるいは UWP 系の Windows.UI.StartScreen.JumpList が使われているのかと考えるが、これらは全く使われていない。

内部では、より直接的な非公開 COM / WinRT API が使われている。先に行っておくと、これらの API はレジストリに登録されているため、私たちでも使うことができる。

flowchart TD
    A["ShellExperienceHost.exe"] --> B["JumpViewUI.dll<br/>Jump List UI"]
    B --> C["StartTileData.dll<br/>WindowsInternal.Shell.JumpList.*"]
    C --> D["Automatic Destinations<br/>IAutomaticDestinationList2"]
    C --> E["Custom Destinations<br/>IInternalCustomDestinationList"]
    D --> F["windows.storage.dll<br/>CAutomaticDestinationList / parser"]
    E --> G["windows.storage.dll<br/>CDestinationList"]

もともとは、ジャンプリストの UI 自体は Windows 7 の頃は explorer.exe に実装されており、WinRT API もなく、直接 shell32.dll にあった IAutomaticDestinationList 系や IInternalCustomDestinationList を使っていた。今はより使い勝手のいい WinRT API が仲介し、それでも今でも COM API を低レベルで使っている。

つまり、WinRT が COM を完全に置き換えたわけではなく、WinRT は上位のオブジェクトモデルと broker を提供し、実データの取得は下の COM 層へ橋渡ししている。

Automatic Destinations と Custom Destinations は別物

Jump List を理解するうえでまず重要なのは、Automatic Destinations と Custom Destinations を分けることだ。

  • Automatic Destinations

システム管理の Recent / Frequent / Pinned

  • Custom Destinations

アプリが自前でカテゴリを組んで保存する独自項目群

  • Tasks

Custom Destinations 側の特別なカテゴリ

内部実装も分かれている。

flowchart LR
    A["Jump List categories"] --> B["Pinned"]
    A --> C["Recent"]
    A --> D["Frequent"]
    A --> E["Custom"]
    A --> F["Tasks"]

    B --> G["Automatic Destinations list"]
    C --> G
    D --> G

    E --> H["Custom Destinations list"]
    F --> H

StartTileData.dllJumpListCategoryKind は次の値を使っていた。

[flags]
enum JumpListCategoryKind : uint32_t
{
    Pinned   = 0x1,
    Recent   = 0x2,
    Frequent = 0x4,
    Tasks    = 0x8,
    Custom   = 0x10,
};

automatic 側 selector への変換も見えている。

  • `Pinned(1) -> 0`
  • `Recent(2) -> 1`
  • `Frequent(4) -> 2`

`windows.storage.dll`: Automatic Destinations の本体

Automatic Destinations の COM 実装本体は windows.storage.dll にある CAutomaticDestinationList だ。

CAutomaticDestinationList
    : IAutomaticDestinationList
    , IAutomaticDestinationList2
    , IAutomaticDestinationList3
    , IAutomaticDestinationList4
    , IAutomaticDestinationListPrivate
    , IAutomaticDestinationListPropertyStore
{ }

このビルドでは 34 に追加 slot は見えず、ABI は 2 と同じだった。つまり実質的な拡張は IAutomaticDestinationListPrivateIAutomaticDestinationListPropertyStore に存在する。

IAutomaticDestinationListPrivate は内部メンテナンス用 API を持つ。

  • `SetDebounce`
  • `SetInitialization`
  • `SetUpgrade`
  • `SetSuspendRibbonUpdates`
  • `GetDecayThreshold`
  • `GetListSize`
  • `GetItemData`
  • `GetItemName`
  • `ClearCache`
  • `SetMigrationOrderStream`
  • `GetItemTarget`

IAutomaticDestinationListPropertyStore はアイテムやリストの IPropertyStore を読む層だ。

  • `GetPropertyStorageForItem`
  • `SetPropertyStorageForItem`
  • `GetPropertyForItem`
  • `SetPropertyForItem`
  • `GetPropertyStorageForList`
  • `SetPropertyStorageForList`
  • `GetPropertyForList`
  • `SetPropertyForList`

ここから分かるのは、Automatic Destinations は単なる「最近使ったパスの配列」ではなく、IPropertyStore と ピン留め情報を持つかなり厚いデータモデルだということだ。

`windows.storage.dll` には Automatic Destinations 用 WinRT API もある

面白いのは、windows.storage.dll に internal WinRT も存在することだ。

公開 API ではなく Windows.Internal.* 名前空間だが、Automatic Destinations を WinRT ABI で扱うクラスがある。

見えているのはこれ。

  • `Windows.Internal.AutomaticDestinationListStorage`
  • `Windows.Internal.AutomaticDestinationListItemInfo`

概念的な定義はこう見える。

namespace Windows.Internal
{
    enum DestinationListAccess
    {
        ReadOnly  = 0,
        ReadWrite = 1,
    };

    runtimeclass AutomaticDestinationListItemInfo
    {
        Double UsagePoints;
        DateTime LastUsed;
        UInt32 ActionCount;
        Int32 PinPosition;
        ValueSet ExtendedProperties;
    };

    runtimeclass AutomaticDestinationListStorage
    {
        UInt32 ItemCount { get; }
        UInt32 PinnedItemCount { get; }

        IStorageItem GetItemAtIndex(UInt32 index);
        AutomaticDestinationListItemInfo GetInfoAtIndex(UInt32 index);

        void Load(DestinationListAccess access, String exePath, String appId, String customDestFilePath);
        void Save();
        void Close();
    };
}

ここで重要なのは、画像はこの層では返さない という点だ。

返るのは IStorageItem と metadata であって、アイコンバイト列ではない。

つまり icon を得たいなら、この storage 層ではなく、より上の Jump List 表示モデルか、別の shell image API を使う必要がある。

`windows.storage.dll`: Custom Destinations の本体

Custom Destinations は別の COM クラス CDestinationList で実装されている。こちらは Automatic Destinations とは別系統だ。

主な interface は以下。

  • `ICustomDestinationList`

{6332DEBF-87B5-4670-90C0-5E57B408A49E}

  • `IInternalCustomDestinationList`

{507101CD-F6AD-46C8-8E20-EEB9E6BAC47F}

CLSID はこれ。

  • `CLSID_DestinationList`

{77F10CF0-3DB5-4966-B520-B7C54FD35ED6}

IInternalCustomDestinationList は公開 ICustomDestinationList に対して、既存 list の読み出し・編集を足した内部 interface だ。

interface IInternalCustomDestinationList : IUnknown
{
    HRESULT SetMinItems(UINT minItems);
    HRESULT SetApplicationID(PCWSTR appId);
    HRESULT GetSlotCount(UINT* slots);
    HRESULT GetCategoryCount(UINT* count);
    HRESULT GetCategory(UINT index, GETCATFLAG flags, APPDESTCATEGORY* outCategory);
    HRESULT DeleteCategory(UINT index, int mode);
    HRESULT EnumerateCategoryDestinations(UINT index, REFIID riid, void** ppv);
    HRESULT RemoveDestination(IUnknown* item);
    HRESULT ResolveDestination(HWND hwnd, UINT resolveFlags, IShellItem* item, REFIID riid, void** ppv);
    HRESULT HasListEx(BOOL* hasAnyItems, int* hasTaskCategory);
    HRESULT ClearRemovedDestinations();
};

category type は実装上こう見えている。

  • `0 = custom named category`
  • `1 = known category`
  • `2 = tasks`

KNOWNDESTCATEGORY はこのビルドでは 1, 2, -1 が許容されていた。

  • `1 = Frequent`
  • `2 = Recent`
  • `-1 = internal sentinel の可能性が高い`

ここでのポイントは、Custom Destinations には WinRT runtime class が見えていない ことだ。

automatic は internal WinRT を持っているが、custom は COM が本体のまま残っている。

`StartTileData.dll`: Jump List の WinRT API

Jump List 全体のオブジェクトモデルは StartTileData.dll にある。ここが今回いちばん重要な層だ。

見えているランタイムクラスは次の通り。

  • `WindowsInternal.Shell.JumpList.JumpList`
  • `WindowsInternal.Shell.JumpList.JumpListCategory`
  • `WindowsInternal.Shell.JumpList.JumpListItem`
  • `WindowsInternal.Shell.JumpList.JumpListTile`
  • `WindowsInternal.Shell.JumpList.JumpListEnumerationContext`
  • `WindowsInternal.Shell.JumpList.JumpListItemActivationContext`
  • `WindowsInternal.Shell.JumpList.JumpListUpdatesAvailableEventArgs`
  • `WindowsInternal.Shell.JumpList.Broker.JumpListBroker`

概念的にはこういうモデルになっている。

classDiagram
    class JumpList {
        +EnumerateAsync(tile, arg, context)
        +Tile
        +Categories
        +Policy
        +CurrentUpdateId
        +AvailableUpdateId
        +ExtendedProperties
        +UpdateAsync()
    }

    class JumpListCategory {
        +Kind
        +DisplayName
        +Items
    }

    class JumpListItem {
        +DisplayName
        +Description
        +Path
        +Timestamp
        +Logo
        +Kind
        +IsPinned
        +PinIndex
        +ActivateAsync()
        +LoadImageAsync()
        +PinAsync()
        +UnpinAsync()
        +RemoveAsync()
        +ResolveAsync()
    }

    class JumpListTile {
        +UnifiedTileId
        +DisplayName
        +Logo
        +PackageFullName
        +ResolvedAppPath
        +ActivateAsync()
        +LoadImageAsync()
    }

    JumpList --> JumpListCategory
    JumpListCategory --> JumpListItem
    JumpList --> JumpListTile

ここで初めて、UI に必要な「category」「item」「tile」「logo」「activation」がまとまってくる。

---

JumpList は direct activation ではない

WindowsInternal.Shell.JumpList.JumpListRoActivateInstance で直接 new する型ではない。

statics/factory 経由で使う。

statics IID はこれ。

  • `IJumpListStatics`

{7758FE16-E384-453C-80D9-C07F90879C2C}

入口は EnumerateAsync(...) だ。

IAsyncOperation<JumpList> EnumerateAsync(
    IUnifiedTileIdentifier tile,
    hstring arg,
    JumpListEnumerationContext context);

概念的な呼び方はこうなる。

auto factory = RoGetActivationFactory(
    L"WindowsInternal.Shell.JumpList.JumpList",
    IID_IJumpListStatics);

auto op = factory->EnumerateAsync(tile, arg, context);

ただしこれは公開 WinRT ではない。

さらに下で broker の caller check が入るので、普通のアプリからそのまま使える API とは見なせない。

---

StartTileData.dll は下の COM 実装をラップしている

StartTileData.dll は上位の WinRT object model を提供しているが、データ取得自体は下の COM 層を使う。

automatic 側では:

  • `CreateAutomaticDestinationList`
  • `IAutomaticDestinationList2`

custom/tasks 側では:

  • `CreateCustomDestinationList`
  • `IInternalCustomDestinationList`

つまり中の流れはこうだ。

sequenceDiagram
    participant UI as jumpviewui.dll
    participant JL as StartTileData.dll JumpList
    participant Broker as JumpListBroker
    participant Auto as IAutomaticDestinationList2
    participant Custom as IInternalCustomDestinationList
    participant Store as windows.storage.dll

    UI->>JL: IJumpListStatics::EnumerateAsync(...)
    JL->>Broker: EnumerateAsync(...)
    Broker->>Auto: automatic categories 
    Broker->>Custom: custom/tasks categories 
    Auto->>Store: automatic destination parser / storage
    Custom->>Store: custom destination file handling
    Broker-->>JL: JSON + item broker map
    JL-->>UI: JumpList object graph

逆に言うと、Jump List の上位 WinRT surface を見ていても、その下では今でも classic shell/COM 文化が生きている。

---

broker が caller を制限している

内部実装で特に重要なのが broker だ。

JumpListBroker::EnumerateAsync 側には access check がある。

見えている分岐は次の 2 系統。

  • `FullTrustStartMenu` capability を持つクライアント
  • `CallerIdentity::EnsureCallingProcessIsShellExperience()`

このため、WindowsInternal.Shell.JumpList.* は「型定義さえ分かれば誰でも使える hidden WinRT API」ではない。

実際には Windows 内部クライアント向けの面であり、broker で gate される。

この構造は、internal WinRT に見せつつも、実行主体を shell 系プロセスに限定したいという設計に見える。

---

jumpviewui.dll は実際に何を呼んでいるのか

Jump List UI クライアントである jumpviewui.dll は、StartTileData.dll の lower-level broker ではなく、上位 JumpList statics を呼んでいる。

直接の呼び出し元は:

  • `JumpViewUI::JumpListViewModel::InitializeAsync`

流れはこうだった。

1. JumpListSession から値を取る

2. JumpListEnumerationContext を作る

3. context.TargetAppId = session.TargetAppId

4. RoGetActivationFactory("WindowsInternal.Shell.JumpList.JumpList", IID_IJumpListStatics, ...)

5. IJumpListStatics::EnumerateAsync(session.UnifiedTileId, session.TargetAppPath, context)

6. IAsyncOperation を task 化

7. 完了後 InitializeList(...)

8. UpdatesAvailable を購読

9. UpdateContents(...) で ViewModel へ展開

図にするとこうなる。

flowchart TD
    A["JumpListSession"] --> B["TargetAppId"]
    A --> C["UnifiedTileId"]
    A --> D["TargetAppPath"]

    E["JumpListViewModel::InitializeAsync"] --> F["new JumpListEnumerationContext()"]
    F --> G["context.TargetAppId = session.TargetAppId"]

    E --> H["RoGetActivationFactory<br/>WindowsInternal.Shell.JumpList.JumpList"]
    H --> I["IJumpListStatics"]

    C --> J["EnumerateAsync(...)"]
    D --> J
    G --> J
    I --> J

    J --> K["IAsyncOperation<JumpList>"]
    K --> L["create_task(...)"]
    L --> M["InitializeList(...)"]
    M --> N["UpdateContents(...)"]

ここで重要なのは、2 番目の string 引数が単なる曖昧な opaque arg ではなく、少なくとも jumpviewui.dll の caller では TargetAppPath である点だ。

---

JumpListSession は tile と app identity をまとめる

JumpListSession は Jump List query に必要な文脈を持つオブジェクトだった。

逆解析上の主要フィールドはこう見える。

  • `User`
  • `UnifiedTileId`
  • `TargetAppId`
  • `TargetAppPath`
  • `JumpViewSurface`
  • `JumpViewInvocationMethod`

さらに TargetAppId は単純な app id ではなく、UnifiedTileId から composite 化される。

  • packaged tile:

AppUserModelId

  • secondary tile:

AppUserModelId:TileId

  • win32 tile:

win32 側の AppUserModelId

これは、Jump List が単純な exe path ベースではなく、tile identity ベースでルーティングされていることを示している。

---

アイコンはどこから来るのか

automatic destination storage 層だけを見ていると、アイコンの出所が分かりにくい。

理由は単純で、windows.storage.dll の storage WinRT は画像を返さないからだ。

画像やロゴを扱うのは上位の JumpListItem / JumpListTile 側だ。

  • `JumpListItem.Logo`
  • `JumpListItem.LoadImageAsync(...)`
  • `JumpListTile.Logo`
  • `JumpListTile.LoadImageAsync(...)`

つまり icon の取得は「automatic destination parser」の責務ではなく、「Jump List 表示モデル」の責務に寄っている。

内部で最終的にどの image pipeline を踏むかは別途さらに追えるが、少なくともレイヤ分割としてはそこが明確だ。

---

設計として見ると何が面白いのか

この構造で面白いのは、Windows が Jump List を完全に WinRT に作り替えたわけではないことだ。

実際には:

  • 永続化と列挙は古い shell/COM モデルが強い
  • その上に internal WinRT object model を載せている
  • UI クライアントは internal WinRT を使う
  • broker で caller を制限している

つまりアーキテクチャとしては「COM を捨てた」のではなく、「COM の上に internal WinRT façade を作った」と見るのが正確だ。

flowchart LR
    A["persistent data / parser"] --> B["COM layer"]
    B --> C["internal WinRT broker"]
    C --> D["internal WinRT object model"]
    D --> E["JumpView UI"]

これは Windows 内部実装ではかなり自然な形で、既存 shell infrastructure を生かしつつ、新しい UI 側では WinRT 的な非同期オブジェクトモデルを使える。

---

まとめ

Jump List の内部実装をざっくりまとめるとこうなる。

  • Automatic Destinations の本体は `windows.storage.dll` の `CAutomaticDestinationList`
  • Custom Destinations の本体は `windows.storage.dll` の `CDestinationList`
  • automatic 側には `Windows.Internal.AutomaticDestinationListStorage` という internal WinRT もある
  • ただし Jump List 全体の object model は `StartTileData.dll` の `WindowsInternal.Shell.JumpList.*`
  • UI クライアント `jumpviewui.dll` は `IJumpListStatics::EnumerateAsync(...)` を使って `JumpList` を取得する
  • その下では依然として `IAutomaticDestinationList2` と `IInternalCustomDestinationList` が使われている
  • この internal WinRT は broker で gate されており、一般公開 API として使う想定ではない

結局のところ、Jump List は「WinRT で書き直された shell 機能」ではなく、

古い destination list 基盤の上に internal WinRT surface を被せ、UI からはそれを使うようにした多層構造 だと言える。