Стимулом к написанию данной статьи стали многочисленные вопросы в форумах, а
так же постоянные нападки одной молодой особы :)
Хочу сразу оговориться и сказать, что нижеследующий текст предназначен для
ещё не совсем опытных , но уже достаточно "продвинутых" программистов. Он не
претендует на полноту изложения по заданной теме и на "рэпирность" определений и
высказываний, используемых автором.
На данный момент существует уже большое количество источников, в которых
можно почитать много интересной информации о принципах написания и работе с
библиотеками, но всё же, начиная разговор о использовании динамически
компонуемых библиотеках, стоит ввести некоторые базовые определения и понятия,
без которых дальнейшие выкладки будут не поняты людям, не имевшими дело с dll, а
тем, кто уже имеет некоторый опыт в данной области будет не лишним освежить в
памяти основы.
Итак, dll - это, содержащие код, ресурсы или какие-то иные данные,
программные модули. Чем нас не устраивают обычные exe-файлы, спросите вы.
Отвечу,- основной смысл dll состоит в том, что ваши приложения могут загружать и
обрабатывать код из библиотеки в процессе работы, а не на этапе сборки. ).
Аббревиатура DLL говорит сама за себя – Dynamic Linked Library, ДИНАМИЧЕСКИ
компонуемая библиотека. По структуре своей dll схожа с exe файлами, но когда
ваша программа использует некую dll, другая программа тоже может использовать
эту же dll, при этом в памяти будет физически загружена только одна копия
используемой библиотеки, а адресное пространство, выделенное вашей программе
будет содержать образ/слепок/memory-mapped_file/экземпляр загруженной
библиотеки.
Не стоит забывать, что процессу, загрузившему dll, доступны функции явно
предоставляемые самой dll для «внешнего мира» - т.е. exports ф-ии.
Так... теперь пару слов о способах загрузки dll. Не секрет, что существует
два способа загрузки библиотек: статический и динамический (в литературе можно
встретить названия - явный и неявный, но я предпочитаю придерживаться
борландовской терминологии). Статический используется, как правило тогда, когда
в вашей библиотеке небольшое количество ф-ий и процедур, которые вы наверняка
собираетесь использовать в вашей программе. Если же ваша библиотека содержит
большое количество процедур/функций или процедуры, вызываемые вами, используются
вашей программой не часто (к примеру, имеет место сложная обработка графического
изображения), то в данном случае целесообразнее использовать динамическую
загрузку, дабы не загромождать память. К минусам статической загрузки можно
отнести тот факт, что если при попытке вашей программы загрузить dll эта
библиотека не будет найдена - вы получите ошибку, а программа просто-напросто не
запустится. Если вы используете динамическую загрузку, то программа запустится в
любом случае, но в момент, когда вы попробуете использовать функцию из
отсутствующей dll, возникнет исключение, которое можно программно обработать и
продолжить выполнение программы.
Статическая загрузка ... implementation
function ShowMyDialog(Msg: PChar): Boolean; stdcall; external 'project1.dll';
procedure SetValue(); cdecl; external 'some.dll'; ...
Тут, кажется не должно быть никаких неясностей - указываем название ф-ии или
процедуры, параметры, возвращаемый тип (для ф-ии), способ передачи аргументов:
stdcall - используется при вызовах WinAPI ф-ий (передача параметров справа на
лево), cdecl - при использовании dll, написанных на C++ ; есть ещё некоторые
соглашения о вызовах о которых предлагаю вам почитать в справочной системе
Delphi. Важно помнить, что если при разработке dll использовалось соглашение
stdcall, то и при её вызове должно использоваться тоже stdcall ! Директива
external указывает на местоположение библиотеки из которой вы хотите
экспортировать ф-ию.
Динамическая загрузка: ... uses ... type { определяем процедурный тип, отражающий экспортируемую процедуру или ф-ию } TMyProcType = procedure(flag: Boolean); stdcall; TMyFuncType = function(Msg: PChar): Boolean; cdecl; { эту операцию можно сделать непосредственно в разделе var процедуры, в которой вы будите загружать dll } ...
procedure TForm1.Button1Click(Sender: TObject); var SetValue: TMyProcType; { объявляем переменные процедурного типа} ShowDialog: TMyFuncType;
// или можно было сделать так (в таком случае type... не нужен): // SetValue: procedure (flag : Boolean); stdcall; // ShowDialog: function (Msg: PChar): Boolean; cdecl;
Handle01, Handle02: HWND; { дескрипторы, загружаемых библиотек }
begin Handle01 := LoadLibrary(PChar('project1.dll')); { загрузка dll } try @ShowDialog := GetProcAddress(Handle01, 'ShowMyDialog'); { получаем указатель на необходимую процедуру} ShowDialog(PChar('Dll function is working !!!')); { используем ф-ию } except ShowMessage('Ошибка при попытке использовать dll !'); finally FreeLibrary(Handle01); { выгружаем dll } end; { try } end;
end.
Аналогично и с процедурой SetValue();
Из всего вышесказанного необходимо понять, что при статической загрузке
экспортируемая вами dll находится в памяти с момента запуска прогарммы вплоть до
Application.Terminate и вам, как программисту, не надо следить за процессом
освобождения памяти из под вашей dll; при динамической загрузке вы сами в любом
месте программы загружаете dll, но и сами же должны следить за её выгрузкой.
Если при динамическом способе загрузки вы самостоятельно, с помощью функции
FreeLibrary, не выгрузите dll из памяти, то она будет находиться в адресном
постранстве процесса, загруженного из вашей программы вплоть до его, процесса,
уничтожения, но в момент завершения работы программы память из под библиотеки
будет освобождена, т.к. сама dll может находиться только в адресном пространстве
загрузившего её процесса, но никогда сама по себе !
Итак, считаем, что с ликбезом покончено. Для чего нужны и как устроены
динамически компануемые библиотеки можно прочитать у Тайксейры - там вся теория
дана в лучшем виде. Совсем неопытным в написании библиотек, а так же начинающим
программистам - настоятельно рекомендую ознакомиться с общими принципами
создания dll, описанными в статье Использование и создание DLL в Delphi. Статьи
Королевства по теме. Для тех кто предпочитает C++ рекомендую прочитать статью
Криса Касперски. После ознакомления - обязательно возвращайтесь ;)
Ну а теперь, пожалуй, приступим к самой теме данной статьи, а конкретнее к
примерам применения и разъяснению этих примеров в меру авторской компетенции ;)
Содержание:
- Регистрация dll в системе ( или как бы нам, воспользовавшись особенностями
приложения Explorer.exe, заставить его загрузить нашу библиотеку в его адресное
пространство).
- Размещение модальных форм в dll.
- Размещение "готовых" файловых образов библиотек в EXE-файле с последующим их
извлечением и использованием.
- Как в своих dll использовать процедуры и ф-ии, находящиеся в других
библиотеках.
- Удаление программы "во время исполнения".
- Ловушки в dll.
Регистрация dll в системе
( или как бы нам, воспользовавшись особенностями приложения Explorer.exe,
заставить его загрузить нашу библиотеку в его адресное пространство)
Часто возникают вопросы такого типа: "Как мне зарегистрировать свою dll в
системе ? Как загрузить свою dll при загрузке компьютера ?"
Для регистрации вашей dll в системе, необходимо:
- в HKEY_CLASSES_ROOT\CLSID задать новый идентификатор класса (GUID).
- в
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ShellServiceObjectDelayLoad
создать раздел InProcServer32 и в значение этого раздела записать путь к вашей
dll, которая должна грузиться при загрузке Windows.
- создать строковый параметр MyDllLoad со значением, равным определённому нами
GUID.
Вот собственно и всё - теперь explorer, при каждом входе в систему, будет
грузить вашу библиотеку ( ВНИМАНИЕ !!! при таком способе регистрации загруженная
dll не будет выгружаться из памяти до тех пор, пока процесс Explorer’а будет
существовать хотя бы в одном экземпляре !)
Реализация данного способа имеет следующий вид: unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, registry;
type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end;
var Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject); var reg: Tregistry;
begin reg := Tregistry.create; reg.rootkey := HKEY_CLASSES_ROOT;
{Теперь настала пора создать идентификатор для нашей библиотеки. Идентификатор класса - это глобальный уникальный идентификатор, состоящий из шеснадцатиричных цифр. В него входят метка времени создания и адрес платы сетевого интерфейса, попросту говоря MAC, или фиктивный адрес платы при отсутствии оной в вашем компьютере 8) Получить этот идентификатор можно двумя путями: 1). в Delphi, в любом месте редактора кода нажать Ctrl-Shift-G . 2). или воспользоваться API функцией CoCreateGUID();}
try reg.openkey('CLSID\{69502F20-E8CD-11D5-A784-0050BF44BD3B}\InProcServer32', true); reg.writestring('', 'C:\TEMP\MyDll.dll'); reg.closekey; reg.rootkey := HKEY_LOCAL_MACHINE; reg.openkey('Software\Microsoft\Windows\CurrentVersion\ShellServiceObjectDelayLoad', true); reg.writestring('MyDllLoade', '{69502F20-E8CD-11D5-A784-0050BF44BD3B}'); reg.closekey;
finally reg.free; end; {try}
end;
end.
Размещение модальных форм в dll
Этот вопрос не представляется сколь-нибудь интересным и поэтому я буду
предельно краток. Делаете New-> DLL, затем New-> Form, накидываем туда всё
что душе пожелает и поехали: library ModelF;
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Unit1 in 'Unit1.pas' {Form1};
function ShowMyDialog(Msg: PChar): Boolean; stdcall; begin {Создаем экземпляр Form1 формы TForm1} Form1 := TForm1.Create(Application); {В Label1 выводим сообщение Msg} Form1.Label1.Caption := StrPas(Msg); {Возвращаем True если нажата OK (ModalResult = mrOk)} Result := (Form1.ShowModal = mrOk); Form1.Free; end;
exports ShowMyDialog;
begin end.
И сам проект, содержащий вызывающий DLL код: unit main2;
interface
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end;
var Form1: TForm1;
implementation
function ShowMyDialog(Msg: PChar): Boolean; stdcall; external 'project1.dll'; {$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject); begin if ShowMyDialog(PChar('Work !!!')) = TRUE then ShowMessage('TRUE !') else ShowMessage('FALSE !'); end;
end.
Размещение "готовых" файловых образов библиотек в EXE-файле с последующим их
извлечением и использованием.
Представим такую ситуацию: ваша программа использует несколько dll-ок, не
вами написанных, и у вас нет их исходного кода (т.е. непосредственно код
процедур и функций, вами используемых в своей программе, вам не доступен), а вам
ну просто "противопоказано" показывать эти библиотеке пользователю (мало ли -
забудет он их переписать вместе с программой, у dll файлов как правило атрибуты
невидимости стоят; или ещё какая-нибудь причина на то имеется - мало ли ;) В
таком случае предлагается сделать следующее: поместить все используемые
библиотеки непосредственно в код программы, как это делается с ресурсами, а
затем, при запуске программы записывать их куда-либо на диск, использовать и, к
примеру, если в этом есть необходимость - стирать при окончании работы
программы. Вы можете сказать, что в таком случае сам exe файл увеличится в
размерах и что можно использовать ф-ии библиотек, размещённых в exe файле без
переноса их на диск. На это можно ответить только одно - всё это конечно так, но
ведь цель данной статьи наметить подходы к решению проблем, которые могут
возникнуть при решении тех или иных задач, а отнюдь не дать готовые рецепты для
какого-то конкретного случая... Итак приступим:
1). открываем самый мощный текстовый редактор - Блокнот и начинаем ваять :
MYDLL RCDATA mydll.dll
Записываем всё это как Lib.rc
2). Теперь для получения файла-ресурсов компилируем получившийся у нас Lib.rc
: brcc32.exe Lib.rc
3). Получили Lib.res, который необходимо прикрепить к нашему проекту, для
этого используем директиву {$R Lib.res}
Нижеследующий код иллюстрирует как можно прикрепить *.res файл к проекту и
извлечь его при необходимости: unit Unit1;
interface
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end;
var Form1: TForm1;
implementation
{$R *.DFM}{$R Lib.RES}
procedure TForm1.Button1Click(Sender: TObject); var MyDll1: TResourceStream; begin MyDll1 := TResourceStream.Create(hInstance, 'MYDLL', RT_RCDATA); try MyDll1.SaveToFile('duck.dll'); finally MyDll1.Free; end; {try}
end;
end.
В результате сборки получим EXE-файл. При нажатии на кнопку формы будет
создан файл duck.dll в той же директории, из которой была запущена программа
(помните, что если dll-файлы в вашей системе имеют атрибут скрытых,- созданный
duck.dll тоже будет невидимым).
Как в своих dll использовать процедуры и ф-ии, находящиеся в других
библиотеках
Не так давно один многообещающий ребёнок задал мне такой вопрос - "что можно
предпринять, если моя программа статически грузит большое количество библиотек,
и эти библиотеки тоже используют ф-ии, экспортируемые из других dll-ок, при этом
мне не хотелось бы каждый раз прописывать их в главном модуле своей программы,
загромождая код."
Для ответа на этот вопрос необходимо помнить, что связь между вызываемой
ф-ией и её исполняемым кодом устанавливается во время выполнения приложения
путём использования ссылки на конкретную ф-ию dll и при этом не важно, будут ли
эти ссылки объявлены в самом приложении или в каком-то внешнем модуле. К
примеру, посмотрите как выглядит файл windows.pas,- ближе к концу файла вы
можете увидеть каким образом вызываются различные ф-ии из библиотеки
advapi32.dll .
Итак, создаём новый unit и объявляем импортируемые ф-ии и процедуры и
определяем все необходимые типы для ваших dll. К примеру, если у вас есть три
библиотеки: 001.dll, 002.dll, 003.dll и в них находятся n-ое кол-во ф-ий и
процедур, которые вы намереваетесь использовать в своей программе, то создаём
новый unit и... : unit gather_it;
interface
function MyFunc001(i: integer): PChar; procedure MyProc002(); function MyFunc003(): Integer; // .......................... procedure Something();
implementation
function MyFunc001; external '001.dll'; procedure MyProc002; external '002.dll'; function MyFunc003; external '003.dll'; // .......................... procedure Something; external 'some_other.dll';
end.
Как видно из этого примера - в этом unit-е нигде нет реализаций самих ф-ий, а
лишь указания на то где эти ф-ии находятся. Для использования данного модуля
просто прописываем его название (unit gather_it) в раздел uses того модуля в
котором предполагается использовать описанные в unit gather_it ф-ии и процедуры.
Удаление программы "во время исполнения"
В название этого раздела вынесен вопрос, который ну если не каждую неделю
задаётся в любом форуме, посвящённом программированию, то по крайней мере очень
часто. Бывают моменты в жизни каждого программиста, когда надо сделать что-то
быстро, не привлекая особого внимания, не оставляя явных следов и не травмируя
нежную психику пользователя :) Я не буду говорить про ring0 и про реализацию
всего этого на asm-е,- нет, мы пойдём другим путём... 8)
К примеру нам надо добиться того, что бы наша программа при запуске без
всяких проволочек стирала себя физически с винта, при этом продолжая выполнять
какой-то код. В лоб это сделать не получится, но что мешает исполнить
необходимый нам код в контексте какого либо постороннего потока ? В таком случае
запущенный нами перед закрытием нашего процесса (в нашем случае процесса,
загруженного из нашего exe-файла) код сможет удалить нужный нам файл, потому что
данный код будет выполняться в адресном пространстве постороннего потока.
Может объяснение не очень удачное, но посмотрев на реализацию всего этого всё
станет предельно ясно.
Вот алгоритм того, о чём я так долго распылялся: (это один из способов,
наверняка не самый элегантный, но я не отступаю от темы статьи и упорно
продолжаю совать эти грешные dll куда ни поподя :)
Извлечь из нашего exe модуля заранее помещённую туда dll (это мы уже умеем) и
записать эту dll в любое место диска.
Загрузить dll в адресное пространство НЕнашего потока и выполнить процедуру
из этой библиотеки, которая сотрёт исходный файл.
Сделать это можно, используя rundll32.exe примерно следующим образом: -
для начала напишем небольшую библиотеку: // ************************ DLL ************************ library test1;
uses Windows;
procedure test(); stdcall; begin //Sleep(5); {можно задерживать выполнение кода при необходимости 8} DeleteFile(PChar('D:\Project1.exe')); { ВНИМАНИЕ, до тех пор, пока выполняющийся процесс, загруженный из Project1.exe, не выполнит команду FreeLibrary(собственный экземпляр) в Win9x/Me либо UnmapViewOfFile в NT/W2k, вы будете получать ошибку. Связанно это с тем, что эти вызовы ни в том ни в другом виде не вставляются автоматически компилятором Делфи в эпилог приложения. Вместо этого после выполнения эпилогом приложения вызова ExitProcess() ОС автоматически разорвет связь между процессом и файлом, вызвав ту или иную из вышеуказанных API-ф-ций. } FreeLibrary(GetModuleHandle(nil)); end;
exports test;
begin // MessageBox(Handle, 'Library loaded !', 'Yeh... !!!', 0); end. // ************************ DLL ************************
ну а та часть exe файла, которая отвечает за загрузку dll может выглядеть
так: ...
procedure TForm1.Button1Click(Sender: TObject); begin ShellExecute(0, nil, 'rundll32.exe', ' test1.dll,test', nil, SW_ShowNormal); Close; end; ...
Заметьте, что в нашей программе мы нигде саму библиотеку не загружаем.
rundll32.exe запускается следующим образом: rundll32.exe имя_dll, имя_процедуры,
параметры. Таким образом в процедуру test мы можем передавать, к примеру, путь к
файлу, который нужно стереть (не забывайте, что String не применим в случае если
не используется юнит ShareMem, и надо использовать PChar). Из примера видно, что
при запуске Project1.exe из корневого каталога диска D:\ и нажатии на Button1,
rundll32 загрузит библиотеку test.dll (если она тоже лежит в корне диска D:\),
выполнит процедуру test, которая удалит Project1.exe и освободит память. Кстати,
загружаемую dll не видит ни TaskManager ни WinSight32 (это конечно не значит,
что таким образом можно скрыть dll от NtQuerySystemInformation в NT или
CreateToolhelp32Snapshot в Win9x !). Если общий подход ясен и у вас есть
воображение, то можно добиться интересных результатов ;)
< в>
Теперь поговорим о так называемых ловушках (hooks). Тех, кто вообще не имеет
представления о понятии hook-а в Windows-кой интерпретации, я опять же отсылаю к
научно-популярной литературе, потому что подробно объяснять механизм работы
hook-ов я не буду (т.к. эта тема выходит за рамки данной статьи), но из ниже
приведённого кода и комментариев к нему, думаю, будет многое понятно для тех,
кто знаком с термином hook, но кому не приходилось самому их программировать. От
слов к делу. Сначала напишем основную часть кода - dll, в котором будем
устанавливать и снимать hook-и, а так же обрабатывать полученные сообщения: library hook_dll;
uses Windows, Messages;
var SysHook: HHook = 0; Wnd: Hwnd = 0;
{ данная ф-ия вызывается системой каждый раз, когда возникает какое-то событие в dialog box-е, message box-е, menu, или scroll bar-е}
function SysMsgProc(code: integer; wParam: word; lParam: longint): longint; stdcall; begin { Передаём сообщение дальше по цепочке hook-ов. } CallNextHookEx(SysHook, Code, wParam, lParam); { флаг code определяет тип произошедшего события. } if code = HC_ACTION then begin { В wnd кладу дескриптор того окна, которое сгенерировало сообщение.} Wnd := TMsg(Pointer(lParam)^).hwnd; { Проверяю, нажата ли правая кнопка мыши} if TMsg(Pointer(lParam)^).message = WM_RBUTTONDOWN then begin { Раскрываю окно на всю клиентскую область.} ShowWindow(wnd, SW_MAXIMIZE); { Вывожу сообщение.} MessageBox(0, 'HOOK is working !', 'Message', 0); end; end; end;
{ Процедура установки HOOK-а}
procedure hook(switch: Boolean) export; stdcall; begin if switch = true then begin { Устанавливаю HOOK, если он не установлен (switch=true). } SysHook := SetWindowsHookEx(WH_GETMESSAGE, @SysMsgProc, HInstance, 0); { тут: WH_GETMESSAGE - тип hook-а ; @SysMsgProc - адрес процедуры обработки ; HInstance - указывает на DLL, содержащую процедуру обработки hook-а; последний параметр указывает на thread, с которым ассоциирована процедура обработки hook-а; } MessageBox(0, 'HOOK установлен !', 'Сообщение из DLL', 0); end else begin { Снимаю HOOK, если он установлен (switch=false). } UnhookWindowsHookEx(SysHook); MessageBox(0, 'HOOK снят !', 'Сообщение из DLL', 0); SysHook := 0; end; end;
exports hook;
begin //MessageBox(0, 'MESSAGE FROM DLL - loaded !', 'Message', 0); end.
Так... с этим покончили; теперь код модуля, откуда будем вызывать нашу
процедуру hook (для разнообразия буду использовать "динамическую" загрузку dll):
unit Unit1;
interface
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
{для динамической загрузки dll} type MyProcType = procedure(flag: Boolean); stdcall; {*****************************}
type TForm1 = class(TForm) Button1: TButton; Button2: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); private { Private declarations } public { Public declarations } end;
var Form1: TForm1; Hdll: HWND; { дескриптор загружаемой dll-ки (для динамической загрузки)}
implementation { раскоментируйте эту строку для статической загрузки } //procedure hook(state: boolean); stdcall; external 'hook_dll.dll';
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject); var hook: MyProcType; {для динамической загрузки}
begin { раскоментируйте эту строку для статической загрузки } //hook(true);
{ ********* динамическая загрузка **************} Hdll := LoadLibrary(PChar('hook_dll.dll')); { загрузка dll } if Hdll > HINSTANCE_ERROR then { если всё без ошибок, то } @hook := GetProcAddress(Hdll, 'hook') { получаем указатель на необходимую процедуру} else ShowMessage('Ошибка при загрузке dll !'); { **********************************************}
hook(true);
Button2.Enabled := True; Button1.Enabled := False; end;
procedure TForm1.Button2Click(Sender: TObject); var hook: MyProcType; {для динамической загрузки}
begin { раскоментируйте эту строку для статической загрузки } //hook(false);
{ ********* динамическая загрузка **************} hook := GetProcAddress(Hdll, 'hook'); { получаем указатель на необходимую процедуру} { **********************************************} hook(false); { обращение к процедуре }
Button1.Enabled := True; Button2.Enabled := False; end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin FreeLibrary(Hdll); { при закрытии формы - выгружаем dll } end;
end.
Как видно из примера - после установки hook-а, при нажатии правой кнопки мыши
на элементах dialog box-а, message box-а и menu, то окно (окно в Windows
понимании ;), над которым был произведён клик - займёт всю клиентскую область.
Кстати говоря, почти все клавиатурные шпионы работают примерно по тому же
принципу - устанавливают hook на события клавиатуры, и пишут себе в файлик на
диске всё, что пользователь набирает. Но надо понимать, что hook может быть
поставлен не только из dll, но и из обычного приложения.
На этом, пожалуй, и остановимся... Все предложения, пожелания, нарекания и
т.д. присылайте мне, т.е. автору :)
Все примеры опробованы и работают под Win98 (на NT не пробовал).
Алексей Павлов The
adviser: - Digitman Special thanks to: - Fellomena
|