- PVSM.RU - https://www.pvsm.ru -

7-zip — нет времени спешить

7-zip — нет времени спешить - 1

Преамбула

Данной заметки не должно было быть, но мы живем не в идеальном мире.
Есть много пользователей, что предпочли стандартному проводнику Windows альтернативу и наверное еще больше пользователей архиватора 7-zip. Имеет смысл поделиться с ними, подумал я и вот мы здесь.

Опустим лишние подробности, случилось так, что я заметил в используемом файловом менеджере значительную задержку появления окна контекстного меню при выборе большого количества файлов. Даже учитывая что его код меню написан без учета современных рекомендаций MS, лаг был подозрительно большим. И хоть никого не вдохновляет идея потратить личное время на неоплачиваемую работу с непредсказуемым результатом, я решил немного разобраться в причинах.

Что делает обычный эникейщик в подобной ситуации? Конечно же берет в руки первый подвернувшийся профайлер.

Результат
7-zip — нет времени спешить - 2

Итак, локальным виновником оказался метод IContextMenu::QueryContextMenu, а по факту все время съедает... 7-zip.dll
Эта библиотека является расширением оболочки и отвечает за добавление команд архиватора 7-zip в контекстное меню. Не то что ожидаешь увидеть по итогу, но стоит проверить.

Отключаем показ меню архиватора в настройках программы.

7-zip — нет времени спешить - 3

И убеждаемся, что собака порылась именно тут.
Ну раз такое дело, почему бы не проверить WinRar? Ничего похожего не наблюдается, лаг отсутствует...

Присмотримся

Для более чистого эксперимента закинем тестовую директорию на RAMDISK [1], а файловый менеджер заменим на что-то более очевидное, например Total Commander [2].

Сразу стоит отметить, что все изложенное не затрагивает проводник Windows, там своя внутренняя API магия, которая не ограничивается двухступенчатой инициализацией (сначала для первых 16 объектов, чтобы быстро отобразить меню, а после выбора команды, Invoke уже для полного набора). Все остальные файловые менеджеры почти поголовно подвержены проблеме, как впрочем и практически любой другой софт.

Что ж, попробуем вызвать меню в TC для файлов в нашей папке. Для наглядности сделаем количество побольше. Тест на 20k файлах выявил замедление более чем в х20 раз, а именно 3 секунды без 7-zip и более минуты! с ним.

Вообще, по логике вещей, расширения оболочки не должны вмешиваться в процесс создания меню настолько инвазивно.

Скучный спойлер. WinAPI и базовый алгоритм

Как обычно приложения создают контекстные меню.
Приведу упрощенное изложение стандартного алгоритма.

Небольшая отсылка к MSDN
Для большинства не секрет, что ОС для внутренних операций с объектами файловой системы использует не текстовые представления путей к которым мы привыкли, а некие альтернативные идентификаторы которые могут определять и виртуальные объекты.

Идентификатор элемента (функционально эквивалентен имени файлапапки) фактически является структурой [3] SHITEMID. Список таких идентификаторов определяется структурой [4] ITEMIDLIST.

Для API оболочки объекты пространства имен обычно идентифицируются указателем как раз на эту самую структуру - PIDL (Pointer to an ITEMIDLIST [5]).

В общем, изначально, тем или иным образом приложение (например ФМ) должно преобразовать пути в список идентификаторов. Независимо от API (ILCreateFromPath, SHParseDisplayName итп) в конечном счете будет вызван метод интерфейса IShellFolder [6] рабочего стола Desktop.IShellFolder::ParseDisplayName, внутри которого для стандартных путей идентификаторы будут собраны из WIN32_FIND_DATAW [7], тут (на мой взгляд) можно было бы немного оптимизировать, но разработчикам виднее.
На Win10+ этот метод вызывается из windows.storage.dll (CFSFolder::ParseDisplayName), как и ряд методов ниже.

Затем обычно следует цепочка вызовов (упрощенный вариант):

Desktop.IShellFolder::BindToObject    Получение интерфейса (IShellFolder) родительского объекта (другие подобные API все равно вызовут его)
Parent.IShellFolder::GetUIObjectOf    Получение интерфейса (IContextMenu)
IContextMenu::QueryInterface          Для получения указателя более высокого уровня (IContextMenu3,IContextMenu2)
CreatePopupMenu                       Создается само меню
IContextMenu#::QueryContextMenu       Заполнение меню
TrackPopupMenuEx                      Показываем меню

В GetUIObjectOf мы передаем массив указателей на наши относительные идентификаторы и получаем на выходе интерфейс с дефолтным меню.

При, казалось бы, таком простом действии как создание меню, в действительности "за кулисами" происходит просто огромный объем работы.
Описание всего этого "цирка с конями" потянет на цикл отдельных статей, поэтому сфокусируемся на области используемой 7-zip.

Мы уже выяснили, что инициатор проблемной цепочки это QueryContextMenu.
Внутри CDefFolderMenu::QueryContextMenu происходит много разного
В зависимости от флагов поведение различается, однако базово, к пунктам дефолтного меню добавляются новые с объединением.
По типам объектов в списке сканируются соответствующие ветки реестра на наличие статических и динамических обработчиков.

Для динамики вызываются:
CDefFolderMenu::_AddHandlersToDCA
HDXA_QueryContextMenu

Условный CDefFolderMenu:

*IContextMenu                                       ;  0x00  (00)  CDefFolderMenu::vftable{IContextMenu3}
*IServiceProvider                                   ;  0x08  (08)  CDefFolderMenu::vftable{IServiceProvider}
*IObjectProvider                                    ;  0x10  (16)  CDefFolderMenu::vftable{IObjectProvider}
*IShellExtInit                                      ;  0x18  (24)  CDefFolderMenu::vftable{for IShellExtInit}
*IObjectWithSelection                               ;  0x20  (32)  CDefFolderMenu::vftable{IObjectWithSelection}
*IOleWindow                                         ;  0x28  (40)  CDefFolderMenu::vftable{IOleWindow}
*IDefaultFolderMenuInitialize                       ;  0x30  (48)  CDefFolderMenu::vftable{IDefaultFolderMenuInitialize}
*IVerbStateTaskCallBack                             ;  0x38  (56)  CDefFolderMenu::vftable{IVerbStateTaskCallBack}
*IContextMenuBaseInfo                               ;  0x40  (64)  CDefFolderMenu::vftable{IContextMenuBaseInfo}
*IContextMenuForProgInvoke                          ;  0x48  (72)  CDefFolderMenu::vftable{IContextMenuForProgInvoke}
*IDefaultFolderMenuGetStateFromWrapperInstances     ;  0x50  (80)  CDefFolderMenu::vftable{IDefaultFolderMenuGetStateFromWrapperInstances}
*IContextMenuCB                                     ;  0x68  (104) CFSFolder::vftable{IContextMenuCB}
*IDataObject                                        ;  0x70  (112) CFSIDLData::vftable{IDataObject}
*IShellFolder2                                      ;  0x88  (136) CFSFolder::vftable{IShellFolder2}
*hdsaStatics                                        ;  0xB0  (176) HDSA - массив для статики    DSA_Create(sizeof(STATICITEMINFO),  1)
*HDXA                                               ;  0xB8  (184) HDXA - массив для динамики   DSA_Create(sizeof(CONTEXTMENUINFO), 5)
*hdsaCustomInfo                                     ;  0xC0  (192) HDSA - массив SEARCHINFO's   DSA_Create(sizeof(SEARCHINFO),      1)
*pidlFolder.LPITEMIDLIST                            ;  0xC8  (200) PIDL родительской директории
*apidl.LPITEMIDLIST                                 ;  0xD0  (208) Указатель на массив структур ITEMIDLIST
cidl                                                ;  0xD8  (216) Размер массива apidl (количество структур)
*paa.IAssociationArray                              ;  0xE0  (224) CAssocArray::vftable{IQueryAssociations}
*psss.IServiceProvider                              ;  0xE8  (232) CSafeServiceSite::vftable{IServiceProvider}
nKeys                                               ;  0xF0  (240) Количество хэндлов ключей реестра в массиве hkeyClsKeys
hkeyClsKeys[16]                                     ;  0xF8  (248) HKEY - (#DEF_FOLDERMENU_MAXHKEYS = 16) - Массив открытых хэдлов реестра для класса

При сборе инфы реестра заполняются соответствующие пулы данных, в основном это:

  • Ассоциативный массив IAssociationArray (упорядоченный список путей реестра с информацией о типе, обработчиках, псевдонимах команд, иконках итп)

  • Три динамических массива структур - для статических (hdsaStatics), динамических (HDXA) обработчиков и массив SEARCHINFO (hdsaCustomInfo).

Впоследствии при вызове метода CDefFolderMenu::InvokeCommand, если команда (verb) не каноническая (из списка дефолтного меню) и не статика, функция HDXA_LetHandlerProcessCommandEx начнет перебор структур CONTEXTMENUINFO из массива HDXA и будет пробовать вызвать метод IContextMenu::InvokeCommand по указателю в начале структуры. И тут, либо какой-то метод успешно отработает, либо, если массив кончился, CDefFolderMenu::InvokeCommand вернет E_INVALIDARG

На этом этапе нужно подгрузить в адресное пространство процесса библиотеки всех востребованных динамических расширений меню, а в случае 7-zip, это практически всегда. И тут мы подходим к инициализации нашего обработчика.

Стоит держать в уме, что такой обработчик меню для программиста это зона высокой ответственности, ведь данный код будет выполняться при каждом вызове контекстного меню для объектов ФС и писаться он должен с максимальной оптимизацией.

Нужно больше тестов

Проверим сначала на единственном файле.
Создаем пустую папку и в ней пустой текстовый файл, аттачимся к TC и ставим bp 7_zip!DllGetClassObject
Запускаем выполнение процесса и вызываем контекстное меню файла. После остановки делаем трассировку. На данный момент мы увидим лишь портянку со змейкой вызовов много раз повторяющегося кода 7_zip!DllUnregisterServer+offset

Суммарный результат (не полной) трассировки:

Function Name                 Invocations MinInst MaxInst AvgInst
7_zip                                 559       6     696      38
7_zip!DllGetClassObject                 1      44      44      44
7_zip!DllUnregisterServer            1308       3    2741      23
7-zip — нет времени спешить - 4

У меня конечно есть нелюбовь к построению причинно-следственных связей, но даже мне кажется, что в текущем контексте, вызовов для модуля достаточно много. Конечно можно уже пробовать курить исходники и делать какие-то выводы, но мы пока не будем, хотя сырки все равно нужны. Чтобы было красиво и наглядно нам не хватает символов для 7-zip.dll.

Ну, это дело пяти минут.

Забираем с оффсайта архив, в CPP/Build.mak дописываем в CFLAGS /Zi, в LFLAGS /DEBUG и проставим -Od в CFLAGS_OX

Выполняем в командной строке студии (x64 Native)
cd /d X:7z2201-srcCPP7zip
nmake /f makefile CPU=AMD64 (Если сильно спешите, скомпилируйте только dll)

Теперь у нас есть отладочные символы. Добавим путь к ним и сыркам для отладчика.
Также подменим библиотеку в папке архиватора. Повторим трассировку и посмотрим топ.

Результат
Function Name                               Invocations MinInst MaxInst AvgInst
7_zip!UString::Len                                  436       4       4       4
7_zip!operator new                                  380      10      10      10
7_zip!_malloc_base                                  380      19      19      19
7_zip!malloc                                        380       1       1       1
7_zip!operator new[]                                361       1       1       1
7_zip!free                                          287       3       3       3
7_zip!_free_base                                    287       3      13      12
7_zip!operator delete                               287       1       1       1
7_zip!operator delete[]                             287       1       1       1
7_zip!memcpy                                        250      11      46      20
7_zip!wmemcpy                                       234      12      12      12
7_zip!UString::UString                              243      20     128      22
7_zip!UString::SetStartLen                          131      27      27      27

С символами все стало достаточно наглядно. Посмотрим стек на самой часто вызываемой функции.

стек
7_zip!UString::Len:
00007ffb`b3b71690 48894c2408      mov     qword ptr [rsp+8],rcx ss:00000000`01de7cf0=0000000001de7d38
0:000> k
 # Child-SP          RetAddr           Call Site
00 00000000`01de7ce8 00007ffb`b3b71c06 7_zip!UString::Len [X:7z2201-srcCPPCommonMyString.h @ 587] 
01 00000000`01de7cf0 00007ffb`b3b71897 7_zip!CLang::OpenFromString+0x2d6 [X:7z2201-srcCPPCommonLang.cpp @ 71] 
02 00000000`01de7d90 00007ffb`b3b82224 7_zip!CLang::Open+0x1f7 [X:7z2201-srcCPPCommonLang.cpp @ 146] 
03 00000000`01de7e20 00007ffb`b3b82493 7_zip!LangOpen+0x24 [X:7z2201-srcCPP7zipUIFileManagerLangUtils.cpp @ 30] 
04 00000000`01de7e50 00007ffb`b3b82534 7_zip!OpenDefaultLang+0x143 [X:7z2201-srcCPP7zipUIFileManagerLangUtils.cpp @ 257] 
05 00000000`01de7ee0 00007ffb`b3b82339 7_zip!ReloadLang+0x34 [X:7z2201-srcCPP7zipUIFileManagerLangUtils.cpp @ 279] 
06 00000000`01de7f40 00007ffb`b3b7fa30 7_zip!LoadLangOneTime+0x39 [X:7z2201-srcCPP7zipUIFileManagerLangUtils.cpp @ 43] 
07 00000000`01de7f80 00007ffb`b80cfe1f 7_zip!CZipContextMenu::QueryContextMenu+0x20 [X:7z2201-srcCPP7zipUIExplorerContextMenu.cpp @ 535] 
08 00000000`01de8ae0 00007ffb`b80a7b70 SHELL32!HDXA_QueryContextMenu+0x5e7
09 00000000`01de8d80 00000000`005ed2ce SHELL32!CDefFolderMenu::QueryContextMenu+0x5c0
0a 00000000`01de8ef0 00000000`00000001 TOTALCMD64+0x1ed2ce

Тк отладка у нас с исходным кодом, быстро пробежавшись по строчкам, понимаем что к чему. Все эти аллокации происходят при парсинге файла локализации (srcCPPCommonLang.cpp).

Конечно странное решение.
Почему бы не брать строки по указателям из закешированного шаблона?
Зачем каждый раз самоотверженно парсить один и тот же файл локализации (16Кб для ru) при каждом нажатии ПКМ? Медленные строковые операции стоит минимизировать прежде всего. Хотя что я докопался, это даже "не те дройды" которых мы ищем...

Если указать профайлеру путь к полученным символам, мы увидим больше подробностей и там, но ничего принципиально нового, можно сразу открывать отладчик.

Перейдем в TC в тестовую директорию, где файлов побольше.
После срабатывания bp 7_zip!DllGetClassObject поставим точку остановки на SHELL32!DragQueryFileAorW и глянем стек.
На третьей остановке мы уже попадаем в цикл функции и можем отследить всю проблемную цепочку.

Breakpoint 1 hit (вызов #3+) SHELL32!DragQueryFileAorW:
0:000> k
 # Child-SP          RetAddr           Call Site
00 00000000`01de88d8 00007ffb`b8212f43 SHELL32!DragQueryFileAorW
01 00000000`01de88e0 00007ffb`b3b7a063 SHELL32!DragQueryFileW+0x13
02 00000000`01de8920 00007ffb`b3b7a0e9 7_zip!NWindows::NShell::CDrop::QueryFile+0x33 [X:7z2201-srcCPPWindowsShell.h @ 68] 
03 00000000`01de8950 00007ffb`b3b7a18c 7_zip!NWindows::NShell::CDrop::QueryFileName+0x79 [X:7z2201-srcCPPWindowsShell.cpp @ 114] 
04 00000000`01de89a0 00007ffb`b3b7e16d 7_zip!NWindows::NShell::CDrop::QueryFileNames+0x5c [X:7z2201-srcCPPWindowsShell.cpp @ 130] 
05 00000000`01de89f0 00007ffb`b3b7e7f1 7_zip!CZipContextMenu::GetFileNames+0x11d [X:7z2201-srcCPP7zipUIExplorerContextMenu.cpp @ 146] 
06 00000000`01de8aa0 00007ffb`b80cfc44 7_zip!CZipContextMenu::Initialize+0xd1 [X:7z2201-srcCPP7zipUIExplorerContextMenu.cpp @ 177] 
07 00000000`01de8ae0 00007ffb`b80a7b70 SHELL32!HDXA_QueryContextMenu+0x40c
08 00000000`01de8d80 00000000`005ed2ce SHELL32!CDefFolderMenu::QueryContextMenu+0x5c0
09 00000000`01de8ef0 00000000`00004e20 TOTALCMD64+0x1ed2ce
Вся цепочка вызовов с исходным кодом функций
STDMETHODIMP CZipContextMenu::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT dataObject, HKEY /* hkeyProgID */)
{
  _dropMode = false;
  _dropPath.Empty();
  if (pidlFolder != 0)
  {
    #ifndef UNDER_CE
    if (NShell::GetPathFromIDList(pidlFolder, _dropPath))
    {
      NName::NormalizeDirPathPrefix(_dropPath);
      _dropMode = !_dropPath.IsEmpty();
    }
    else
    #endif
      _dropPath.Empty();
  }
  return GetFileNames(dataObject, _fileNames); 🡰
}

🡻

HRESULT CZipContextMenu::GetFileNames(LPDATAOBJECT dataObject, UStringVector &fileNames)
{
  fileNames.Clear();
  if (!dataObject)
    return E_INVALIDARG;

  #ifndef UNDER_CE
  FORMATETC fmte = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
  NCOM::CStgMedium stgMedium;
  HRESULT result = dataObject->GetData(&fmte, &stgMedium);
  if (result != S_OK)
    return result;
  stgMedium._mustBeReleased = true;

  NShell::CDrop drop(false);
  NMemory::CGlobalLock globalLock(stgMedium->hGlobal);
  drop.Attach((HDROP)globalLock.GetPointer());
  drop.QueryFileNames(fileNames);  🡰
  
  #endif

  return S_OK;
}

🡻

void CDrop::QueryFileNames(UStringVector &fileNames)
{
  UINT numFiles = QueryCountOfFiles();
  fileNames.ClearAndReserve(numFiles);
  for (UINT i = 0; i < numFiles; i++)
  {
    const UString s2 = QueryFileName(i); 🡰
    if (!s2.IsEmpty())
      fileNames.AddInReserved(s2);
  }
}

🡻

UString CDrop::QueryFileName(UINT fileIndex)
{
  UString fileName;
  {
    UINT bufferSize = QueryFile(fileIndex, (LPWSTR)NULL, 0);    🡰  вызов чтобы узнать размер буфера
    const unsigned len = bufferSize + 2;
    QueryFile(fileIndex, fileName.GetBuf(len), bufferSize + 1); 🡰  реальный вызов
    fileName.ReleaseBuf_CalcLen(len);
  }
  return fileName;
}

🡻

UINT QueryFile(UINT fileIndex, LPWSTR fileName, UINT fileNameSize)
    { return ::DragQueryFileW(m_Object, fileIndex, fileName, fileNameSize); }

🡻

SHELL32!DragQueryFileW

🡻

┌─────────────────────────────────────────────────────┐
│    4     0 [  0] SHELL32!DragQueryFileW             │
│   17     0 [  1]   SHELL32!DragQueryFileAorW        │
│   15     0 [  2]     KERNEL32!GlobalLock            │
│   28     0 [  3]       KERNEL32!IsBadReadPtr        │
│   22    28 [  2]     KERNEL32!GlobalLock            │
│   40    50 [  1]   SHELL32!DragQueryFileAorW        │
│    1     0 [  2]     KERNEL32!lstrlenWStub          │
│  654     0 [  2]     KERNELBASE!lstrlenW            │
│   60   705 [  1]   SHELL32!DragQueryFileAorW        │
│ 2181     0 [  2]     SHELL32!StringCchCopyW         │
│   68  2886 [  1]   SHELL32!DragQueryFileAorW        │
│   15     0 [  2]     KERNEL32!GlobalUnlock          │
│   73  2901 [  1]   SHELL32!DragQueryFileAorW        │
│    6     0 [  2]     SHELL32!_security_check_cookie │
│   83  2907 [  1]   SHELL32!DragQueryFileAorW        │
│    6  2990 [  0] SHELL32!DragQueryFileW             │
└─────────────────────────────────────────────────────┘

Коротко по сути - мы кликаем ПКМ на файлах (например хотим посмотреть свойства, или удалить), TC инициализирует процесс создания меню и подгружает расширения, дело доходит до 7-zip...
И тут 7-zip такой:
- Минуточку! Есть мнение, что вы недостаточно страдаете от оверинжиниринга реализации контекстного меню MS и я буду в буфер получать строки файловых путей для каждого выбранного объекта. Мне ведь надо проверить, что вы там выбрали и соответствующие пункты меню показать, чтобы все "по красоте".
- Кто вам сделал удобное меню?
- Кто молодец? А?... вот то-то же!

Безусловно стояла задача сделать пользователю "удобно", но стоило бы потестить и конечную реализацию удобняшек. А по факту мы имеем много бесполезной работы съедающей время.

Также в CDrop::QueryFileName вместо того чтобы один раз выделить буфер достаточного размера, вся цепочка с DragQueryFileW для каждого объекта вызывается дважды, умножая и мою печаль.

В общем, стоит все таки иногда прислушиваться к рекомендациям MS, как это делает WinRar и не пытаться объять "необъятное".

Псевдокод из функции в rarext.dll (расширение WinRar)
ObjCount = DragQueryFileW(hDrop, 0xFFFFFFFF, 0i64, 0);
if ( !ObjCount )
   return E_FAIL;
sub_39D20(a1 + 24);
Count = 16;                                 ✓
if ( ObjCount < 16 )                        ✓
   Count = ObjCount;                        ✓
for ( i = 0; i < Count; ++i )               ✓
{
   szFile[0] = 0;
   DragQueryFileW(hDrop, i, szFile, 0x800u);
   sub_39DC0(a1 + 24, szFile);
}

Причины найдены. Делаем выводы.

Ситуация неоднозначная, поскольку с одной стороны это проблема подавляющего большинства софта, мало кто следует гайдлайнам и мы имеем медленные меню. Но с другой, 7-zip забивший на рекомендации, вносит дополнительные задержки.

Как и где поправить код для 7zip (тем кто захочет это сделать), надеюсь понятно.
Сам я еще тот гуманитарий и поэтому оставлю поиск и разбор прочих недоработок в сырках архиватора тем кто разбирается в вопросе и кому это интересно.

Тестирование производилось на

  • последнем ванильном билде 7-zip v22.01 от 2022-07-15

  • Windows 10 Pro 21H2 19044.1320 x64, CPU 3800МГц

При желании быстро воспроизвести похожий результат (задумайтесь о жизни, если оно возникло), можете воспользоваться командным файлом для создания тестовой директории.

Например, таким
@echo off
cd /d "%~dp0"
set TestDir=%~dp0TEST
md "%TestDir%"
echo "Working..."
for /L %%N in (1,1,20000) do REM. >"%TestDir%%%N.txt"

При запуске создаст рядом с собой папку TEST с 20k текстовыми файлами нулевого размера.
Затем открываете любой ФМ кроме штатного проводника, Ctrl+A и ПКМ.

Многие пользователи TC скорее всего не используют архиватор "классическим" образом, но это совершенно не важно в рамках рассматриваемой проблемы.

Можно было бы обойтись без отладки и сразу смотреть исходники, но часто ситуация бывает не столь однозначна, как выглядит на первый взгляд и в попытке сэкономить десяток минут, можно потратить больше, или что-то упустить.

Кто-то может сказать, что все это ерунда и можно в общем-то игнорировать при современных мощностях железа, а с действительно большими количествами файлов мало кто работает. Это ваш выбор, пусть этой зимой вам будет чуть теплее, чем остальным, просто чаще вызывайте меню 😜

Мораль проста - не все динамические расширения одинаково полезны, а если вы их пишите, надеюсь, умеете "в оптимизацию".
Ну и убирайте всё неиспользуемое из меню (ShellExView [8] в помощь).

Автор: Влад Волков

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/windows/381482

Ссылки в тексте:

[1] RAMDISK: https://ru.wikipedia.org/wiki/RAM-%D0%B4%D0%B8%D1%81%D0%BA

[2] Total Commander: https://www.ghisler.com/

[3] структурой: https://learn.microsoft.com/en-us/windows/win32/api/shtypes/ns-shtypes-shitemid

[4] структурой: https://learn.microsoft.com/en-us/windows/win32/api/shtypes/ns-shtypes-itemidlist

[5] Pointer to an ITEMIDLIST: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/cc144089(v=vs.85)#pidls

[6] IShellFolder: https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ishellfolder

[7] WIN32_FIND_DATAW: https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataw

[8] ShellExView: https://www.nirsoft.net/utils/shexview.html

[9] Источник: https://habr.com/ru/post/706790/?utm_source=habrahabr&utm_medium=rss&utm_campaign=706790