Ассемблер под Windows #35

Интерфейсы IMalloc, IShellLink и IPersistFile


Доброго времени суток, уважаемые подписчики. Сегодня мы рассмотрим программу, которая использует интерфейсы IMalloc, IShellLink и IPersistFile для создания ярлыка на произвольный объект в произвольном месте.


Вначале давайте разберёмся, что нам нужно сделать. Нам надо получить адрес объекта, для создания ярлыка и адрес, по которому этот ярлык будет помещён. Далее, нужно получить новый интерфейс IShellLink, используя его методы, задать нужную нам информацию о ярлыке, и сохранить ярлык. Мне кажется, для сбора информации вполне можно использовать диалоговое окно.

#include <resource.h>

1 DIALOG 154, 143, 161, 118
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION
CAPTION "Пример использования интерфейсов"
FONT 10, "Times New Roman"
{
 PUSHBUTTON "Закрыть", 100, 120, 98, 38, 17
 DEFPUSHBUTTON "Создать", 101, 62, 98, 54, 17
 EDITTEXT 104, 9, 19, 100, 14, ES_AUTOHSCROLL
 LTEXT "Место назначени\xFF", -1, 13, 10, 60, 8
 EDITTEXT 105, 9, 50, 100, 14, ES_AUTOHSCROLL
 LTEXT "Объект", -1, 13, 41, 60, 8
 PUSHBUTTON "Обзор", 102, 113, 19, 44, 14
 PUSHBUTTON "Обзор", 103, 113, 50, 44, 14
 EDITTEXT 106, 9, 79, 150, 14
 LTEXT "Описание", -1, 13, 71, 60, 8
}

Напомню, что так как создание файлов ресурсов никак не связанно с ассемблером, все (или почти все) такие файлы я создаю с помощью Borland Resource Workshop и отдельного внимания созданию этих файлов в рассылке не уделяется.
Итак, мы имеем диалоговое окно с тремя полями ввода информации. Для облегчения выбора адреса, так-же, создадим две кнопки "Обзора". При нажатии на первую кнопку нам нужно найти адрес сохранения файла - никаких проблем, используем GetSaveFileName. А вот во втором случае нас устроят и файлы и папки (так как ярлык может быть создан как на файл так и на папку). В этом случае, считаю правильным использовать функцию SHBrowseForFolder с установленным флагом BIF_BROWSEINCLUDEFILES. Проблема заключается в том, что это функция относится к функциям Shell-а, что для нас означает то, что при успешном выполнении функции мы получим не адрес выбранного объекта, а его PIDL... Так как программирование Shell-а я хотел бы рассмотреть отдельно и более детально, поэтому пока нам будет достаточно того, что PIDL вполне можно преобразовать в обычный адрес. Для этого используется функция SHGetPathFromIDList. После того как нужная информация была получена нам следует приступить к самому созданию ярлыка. Для этого нам нужно создать новый IShellLink. Если не существует специальной функции для создания определённого интерфейса (а иногда и тогда, когда такая функция существует) нужно использовать функцию CoCreateInstance, которая должна получать следующие параметры: CLSID ассоциированный с создаваемым объектом, указатель на контрольный IUnknown (нам не нужен), флаги создания нового интерфейса, идентификатор интерфейса и адрес двойного слова, в которое будет сохранён наш интерфейс. Опередив вопросы, хочу заметить, что CLSID и идентификатор интерфейса это заранее известные 16-и байтовые константы. По каким-то причинам их принято записывать как одно двойное слово, два слова и 8 байт, но это для нас не так важно. Интерфейс IShellLink имеет следующие методы:

Метод 4  = GetPath             Возвращает адрес объекта
Метод 5  = GetIDList           Возвращает адрес объекта в формате PIDL-а
Метод 6  = SetIDList           Устанавливает адрес объекта из PIDL-а
Метод 7  = GetDescription      Возвращает описание объекта
Метод 8  = SetDescription      Устанавливает описание объекта
Метод 9  = GetWorkingDirectory Возвращает директорию по умолчанию
Метод 10 = SetWorkingDirectory Устанавливает директорию по умолчанию
Метод 11 = GetArguments        Возвращает параметры, указанные в командной строке
Метод 12 = SetArguments        Задаёт параметры, указанные в командной строке
Метод 13 = GetHotkey           Возвращает горячую клавишу, ассоциированную с ярлыком
Метод 14 = SetHotkey           Устанавливает горячую клавишу, ассоциированную с ярлыком
Метод 15 = GetShowCmd          Возвращает заданные команды начального отображения окна
Метод 16 = SetShowCmd          Задаёт команды начального отображения окна
Метод 17 = GetIconLocation     Возвращает адрес иконки ярлыка
Метод 18 = SetIconLocation     Устанавливает адрес иконки ярлыка
Метод 19 = SetRelativePath     Устанавливает относительный адрес
Метод 20 = Resolve             Используется для редактирования уже созданных ярлыков
Метод 21 = SetPath             Устанавливает адрес объекта

Из них нас пока интересуют лишь SetPath, SetDescription и QueryInterface (это метод IUnkonwn, не надо забывать, что первые три метода каждого интерфейса - методы IUnknown). Думаю, названия первых двух методов говорят сами за себя, а QueryInterface нам понадобится для получения интерфейса IPersistFile, ассоциированного с нашим IShellLink. IPersistFile используется для сохранения объектов на диск и их загрузки с диска. Он имеет следующие методы:

Метод 4 = GetClassID    Возвращает CLSID объекта
Метод 5 = IsDirty       Проверяет, изменялся ли объект с момента последнего сохранения
Метод 6 = Load          Загружает файл и инициализирует объект
Метод 7 = Save          Сохраняет объект на диск
Метод 8 = SaveCompleted пока неважно
Метод 9 = GetCurFile    Возвращает имя файла, ассоциированное с объектом

Используя метод Save этого интерфейса, мы и сохраняем наш ярлык на диск. При использовании этого метода нужно обратить внимание на то, что передаваемые ему строки должны быть UNICODE-строками (UNICODE формат предполагает, что каждый символ строки занимает одно слово, а не один байт. Это позволяет хранить символы из разных кодовых страниц даже в одной строке. Подробнее с использованием UNICODE мы познакомимся позже, при обсуждении многоязыковых приложений). Так как мы используем не UNICODE строки нам нужно будет перевести нашу строку в UNICODE. Для этого можно использовать функцию MultiByteToWideChar (её использование смотри в приложении).

Прежде чем приступить к самой программе хочу заметить, что почти все интерфейсы являются частью COM (Component Object Model), поэтому прежде чем работать с ними нам нужно инициализировать COM библиотеку, а после завершения работы с ней деинициализировать её. Это можно сделать при помощи функций CoInitializeEx (можно использовать и функцию CoInitialize, но microsoft советует не использовать её в новых приложениях) и CoUninitialize. Исключением является IMalloc, который можно использовать и не инициализируя COM библиотеку (microsoft даже советует использовать функции CoTaskMemAlloc и CoTaskMemFree вместо методов Alloc и Free интерфейса IMalloc, но в учебных целях можно этот совет оставить без внимания).

Итак, приступим, наконец, к самой программе:

include kernel32.inc  ;;добавляем описания функций,
include user32.inc    ;макросов и констант
include def32.inc
include macros.inc
include ole32.inc
include shell32.inc
include comdlg32.inc
  .386                ;386-ой процессор, flat память
  .model flat
  option casemap:none ;Различать маленькие и большие буквы

;сегмент данных
_DATA SEGMENT public USE32 'data'
;Коды-идентефикаторы интерфейсов и классов
CLSID_ShellLink  dd  000021401h
                 dw  0, 0
                 db  0c0h, 0, 0, 0, 0, 0, 0, 46h
IID_IShellLinkA  dd  0000214EEh
                 dw  0, 0
                 db  0c0h, 0, 0, 0, 0, 0, 0, 46h
IID_IPersistFile dd  00000010bh
                 dw  0, 0
                 db  0c0h, 0, 0, 0, 0, 0, 0, 46h
;указатели на обработчики WM_COMMAND
Procz dd  ExitProc
      dd  MakeProc
      dd  SaveProc
      dd  BrowseProc
COUNT_COMMANDPROC = (($-(offset Procz)) shr 2) - 1
;фильтр для GetSaveFileName
aFilters db  '.lnk files',0,'*.'
aDefExt  db  'lnk',0,0
_DATA ENDS

;не инициализированные данные
_BSS SEGMENT public USE32 'bss'
IMalloc    dd  ?
BrowseInfo BROWSEINFO<>
SaveName   OPENFILENAME<>
_BSS ENDS

;сегмент кода
_TEXT SEGMENT public USE32 'code'
;Получаем hInstance и вызываем диалог, после завершаем программу
_start:
  null ebx
  run GetModuleHandle, ebx
  run DialogBoxParam, eax, 01h, ebx, offset DlgProc, ebx
  run ExitProcess, ebx
;Процедура диалогового окна
DlgProc proc
  null eax
  push ebp
  mov ebp,esp    ;создаём стековой кадр
  pusha
  mov edi,[ebp+08h]  ;заносим handle окна в edi
  null ebx    ;обнуляем ebx
  mov eax,[ebp+0Ch]  ;заносим в eax сообщение и обрабатываем его
  cmp eax,WM_COMMAND
  je command_proc
  cmp eax,WM_INITDIALOG
  je init_proc
  cmp eax,WM_CLOSE
  je close_proc
  popa
  leave
  ret 16
SaveProc label dword    ;если нажат "обзор" для создаваемого файла
  ;IMalloc -> Alloc(4096)  ;создаём буфер
    push 4096
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+0Ch]

  jeaxz close_proc    ;если ошибка, выходим из программы
  mov [eax],ebx    ;попробуйте убрать эту строку...
  mov SaveName.lpstrFile, eax
  run GetSaveFileName, offset SaveName
  jeaxz EndWndProc    ;Если нажата "отмена", то не делать ничего
  run SetDlgItemText, edi, 104, SaveName.lpstrFile
  ;IMalloc -> Free(SaveName.lpstrFile)  ;уничтожаем буфер
    push SaveName.lpstrFile
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+14h]

  jmp EndWndProc
BrowseProc label dword    ;если нажат "обзор" для объекта
  ;IMalloc -> Alloc(4096)  ;создаём буфер
    push 4096
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+0Ch]

  jeaxz close_proc    ;Если ошибка, то выходим
  mov BrowseInfo.pszDisplayName, eax
  run SHBrowseForFolder, offset BrowseInfo  ;Запрашиваем файл или папку
  jeaxz EndWndProc    ;Если была нажата "Отмена"
    push eax    ;для последующего IMalloc->Free
  run SHGetPathFromIDList, eax, BrowseInfo.pszDisplayName ;PIDL->Строка
  ;IMalloc -> Free(eax)  ;уничтожаем PIDL, который вернула SHBrowseForFolder
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+14h]

  run SetDlgItemText, edi, 105, BrowseInfo.pszDisplayName
  ;IMalloc -> Free(BrowseInfo.pszDisplayName)  ;уничтожаем буфер
    push BrowseInfo.pszDisplayName
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+14h]

  jmp EndWndProc
command_proc:
  movzx ecx,word ptr [ebp+10h]
  sub ecx, 100
  js EndWndProc
  cmp ecx, COUNT_COMMANDPROC
  ja EndWndProc
  jmp dword ptr Procz[ecx*4]
EndWndProc:
  popa
  inc eax
  leave
  ret 16
ExitProc label dword  ;Если нажата кнопка "Закрыть" или Alt+F4
close_proc:
  ;IMalloc -> Release()  ;Уничтожаем наш интерфейс
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+08h]

  run CoUninitialize  ;деинициализируем COM библиотеку
  run EndDialog, edi, ebx  ;уничтожаем диалог
  jmp EndWndProc
MakeProc label dword  ;Нажата кнопка "Создать"
  run AllocMemAndGetText, 104  ;получаем адрес создаваемого ярлыка
  push eax
  run AllocMemAndGetText, 105  ;получаем объект, на который указывает ярлык
  push eax
  run AllocMemAndGetText, 106  ;получаем описание ярлыка
  push eax
  run CreateLink  ;эта функция создаёт ярлык, она получает три параметра,
                  ;но (!) не удаляет их из стека
  pop eax    ;получаем буфера из стека и уничтожаем их
  ;IMalloc -> Free(eax)
    push eax
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+14h]

  pop eax
  ;IMalloc -> Free(eax)
    push eax
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+14h]

  pop eax
  ;IMalloc -> Free(eax)
    push eax
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+14h]

  run MessageBeep, ebx
  jmp EndWndProc
init_proc:    ;при создании диалога
  run CoInitializeEx, ebx, COINIT_APARTMENTTHREADED  ;инициализируем COM библиотеку
  test eax,eax
  js close_proc  ;если ошибка, то выходим
  run CoGetMalloc, 01h, offset IMalloc  ;получаем наш интерфейс IMalloc
  test eax,eax
  js close_proc  ;если ошибка, то выходим
  mov BrowseInfo.hwndOwner,edi  ;инициализируем структуры поиска файлов
  mov BrowseInfo.ulFlags,  BIF_DONTGOBELOWDOMAIN + BIF_NEWDIALOGSTYLE + BIF_BROWSEINCLUDEFILES
  mov SaveName.lStructSize,sizeof(OPENFILENAME)
  mov SaveName.hwndOwner,  edi
  mov SaveName.lpstrFilter,offset aFilters
  mov SaveName.nMaxFile,   2048
  mov SaveName.Flags,      OFN_EXPLORER or OFN_OVERWRITEPROMPT or OFN_PATHMUSTEXIST
  mov SaveName.lpstrDefExt,offset aDefExt
  jmp EndWndProc
DlgProc endp

;Функция создаёт буфер нужного размера и получает
;в него информацию из Edit box-ов диалога

AllocMemAndGetText proc
  push edi
  run GetDlgItem, edi, dword ptr [esp+08h]
  mov esi,eax    ;получаем в esi handle EditBox-а
  run GetWindowTextLength, eax  ;получаем длину текста
  inc eax    ;Буфер должен содержать место и для завершающего строку нуля
  push eax    ;дла функции GetWindowText, см. ниже
  ;IMalloc -> Alloc(eax)  ;создаём буфер
    push eax
    mov eax,IMalloc
    push eax
    mov eax,[eax]
    run dword ptr [eax+0Ch]

  mov edi,eax    ;временно сохраняем адрес буфера в edi
  run GetWindowText, esi, eax  ;считываем текст
  mov eax,edi    ;восстанавливаем адрес буфера
  pop edi
  ret 4
AllocMemAndGetText endp

;Функция создаёт ярлык, она получает три параметра через стек,
;но (!) не удаляет их из стека

CreateLink proc
  enter 408h, 00h  ;создаём стековой кадр резервируя буфер
                   ;для UNICODE-строки и двух интерфейсов
  lea eax,[ebp-04h]  ;получаем IShellLink в [ebp-04h]
  run CoCreateInstance, offset CLSID_ShellLink, ebx, CLSCTX_INPROC_SERVER, offset IID_IShellLinkA, eax
  ;[ebp-04h] -> SetPath(dword ptr [ebp+0Ch])  ;устанавливаем адрес объекта
    push dword ptr [ebp+0Ch]
    mov eax,[ebp-04h]
    push eax
    mov eax,[eax]
    run dword ptr [eax+50h]

  ;[ebp-04h] -> SetDescription(dword ptr [ebp+08h]);устанавливаем описание
    push dword ptr [ebp+08h]
    mov eax,[ebp-04h]
    push eax
    mov eax,[eax]
    run dword ptr [eax+1Ch]

  lea eax,[ebp-08h]    ;получаем IPersistFile
  ;[ebp-04h] -> QueryInterface(offset IID_IPersistFile, eax)
    push eax
    push offset IID_IPersistFile
    mov eax,[ebp-04h]
    push eax
    mov eax,[eax]
    run dword ptr [eax]

  lea esi,[ebp-408h]  ;esi = буфер для UNICODE-строки
                      ;Конвертируем строку в UNICODE

  run MultiByteToWideChar, ebx, ebx, dword ptr [ebp+10h], -1, esi, 512
  ;[ebp-08h] -> Save(esi, 01h)  ;Сохраняем ярлык на диск
    push 01h
    push esi
    mov eax,[ebp-08h]
    push eax
    mov eax,[eax]
    run dword ptr [eax+18h]

  ;[ebp-08h] -> Release()  ;уничтожаем наши интерфейсы
    mov eax,[ebp-08h]
    push eax
    mov eax,[eax]
    run dword ptr [eax+08h]

  ;[ebp-04h] -> Release()
    mov eax,[ebp-04h]
    push eax
    mov eax,[eax]
    run dword ptr [eax+08h]

  leave
  ret
CreateLink endp

  end _start  ;конец программы
_TEXT ENDS

Давайте рассмотрим отдельно действие функции SHBrowseForFolder. Единственным её параметром является адрес заполненной структуры BROWSEINFO.

BROWSEINFO STRUCT
  hwndOwner      dd ? ;окно создатель
  pidlRoot       dd ? ;PIDL дли инициализации
  pszDisplayName dd ? ;буфер для имени выбранного объекта
  lpszTitle      dd ? ;указатель на нестандартный заголовок окна
  ulFlags        dd ? ;флаги
  lpfn           dd ? ;адрес процедуры, вызываемой, если определённое событие произошло
  lParam         dd ? ;передаваемый функции параметр
  iImage         dd ? ;сюда возвращается индекс иконки объекта в системном списке иконок
BROWSEINFO ENDS

Однако, возвращаемое этой функцией значение гораздо интереснее. В случае, если юзер нажал на кнопку "отмена", то функция возвращает ноль, но если был выбран объект, то функцией возвращается указатель на блок памяти, содержащий PIDL данного объекта. Причём задача освобождения этого блока памяти ложится на вызвавшую функцию программу. Так как блок памяти был выделен тем-же механизмом, который использует IMalloc, после перевода PIDL в строку мы закрываем его методом Free нашего интерфейса IMalloc.

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

Скомпилированную программу и всё необходимое для её компиляции можно скачать по следующим адресам:
0035asm - код программы и файл ресурсов
0035inc - файлы дополнения
0035lib - библиотеки импорта
0035exe - скомпилированная программа
compile - необходимые для компиляции программы


Так как личный опыт ни капли не важнее знаний, сегодня я предлагаю вам сделать что-то вроде домашнего задания. Я дам вам небольшое задание и буду ждать от вас результаты ваших трудов. Естественно, это задание не обязательно, но помните, что нельзя научиться программировать просто разбираясь в чужих программах. Программа, которую мы сегодня рассмотрели, является не только достаточно хорошим примером использования интерфейсов, но и примером программы, которой нельзя пользоваться никому, кроме её создателя! Почему? Потому-что в ней отсутствует проверка результатов методов и функций, возврат в стабильное положение, в случае ошибки, и уведомление юзера о случившейся ошибке. Сегодняшнее задание, убрать как можно больше таких недостатков, то есть сделать проверку возвращаемых значений методов и процедур, и, в случае ошибки, по возможности, как-то вернуться к исходному положению. При этом надо сообщить об ошибке юзеру. Далее приведённая таблица может вам помочь...
Функция/МетодЗначение в случае удачиЗначение в случае ошибки
CoInitializeEx0негативное значение (бит 31 = 1)
CoGetMalloc0негативное значение (бит 31 = 1)
IMalloc::Allocуказатель на блок памяти0
CoCreateInstance0негативное значение (бит 31 = 1)
Любой метод IShellLink0негативное значение (бит 31 = 1)
Любой метод IPersistFile0негативное значение (бит 31 = 1)
Далее, следует помнить, что если интерфейс был успешно создан, то он должен быть уничтожен вне зависимости от успешности произведённых с его помощью действий. Теперь дело только за вами. Жду ваши программы в течении двух недель. По прошествии этого срока, самый лучший вариант выполнения этого задания будет опубликован в рассылке (естественно с именем его автора).


На сегодня это всё. Если есть какие вопросы, пишите. Пишите на Dark_Lord@RusFAQ.ru. Предыдущие выпуски можно найти на сайте рассылки

Сайт управляется системой uCoz