Разработка DLL (курсовая работа)
.pdfВам не обязательно самостоятельно определять функцию WEP. Так же как и функция LibEntry, функция WEP добавляется в DLL-библиотеку транслятором. Вы, однако, можете определить собственную функцию WEP, если перед выгрузкой DLL-библиотеки требуется освободить заказанные ранее блоки памяти.
Приведем пример простейшей функции WEP:
int FAR PASCAL WEP(int bSystemExit)
{
return 1;
}
В операционной системе Windows версии 3.0 на использование функции WEP накладывались серьезные ограничения, которые делали ее практически бесполезной. Например, размер стека, используемого для работы этой функции, составлял всего 256 байт, чего совершенно недостаточно для вызова функций программного интерфейса Windows. Есть и другие проблемы, однако мы не будем их затрагивать из-за того, что версия 3.0 уже сильно устарела, а в версии 3.1 все эти ограничения сняты. Поэтому вы можете использовать функцию WEP для любых процедур, в частности, для освобождения памяти, полученной при инициализации в функции
LibMain.
Экспортируемые функции
Кроме функций LibMain и WEP в DLL-библиотеке могут быть определены и другие функции (как мы уже говорили, существуют DLL-библиотеки, состоящие из одних ресурсов). Это могут быть экспортируемые и неэкспортируемые функции.
Экспортируемые функции доступны для вызова приложениям Windows. Неэкспортируемые являются локальными для DLL-библиотеки, они доступны только для функций библиотеки.
Самый простой способ сделать функцию экспортируемой в системе разработки Borland Turbo C++ for Windows - указать в ее определении ключевое слово _export. Мы использовали этот способ при определении функции окна. Для экспортируемой функции создается специальный пролог, необходимый для правильной загрузки сегментного регистра DS.
Есть и другой способ - можно перечислить все экспортируемые функции в файле определения модуля при помощи оператора EXPORTS :
EXPORTS
DrawBitmap @4
ShowAll
HideAll
GetMyPool @8
FreeMyPool @9
В приведенном выше примере в разделе EXPORTS перечислены имена нескольких экспортируемых функций. Около некоторых имен после символа "@" указаны порядковые номера соответствующих функций в DLL-библиотеке.
Если вы не укажите порядковые номера экспортируемых функций, при компоновке загрузочного файла DLL-библиотеки редактор связи создаст свою собственную нумерацию, которая может изменяться при внесении изменений в исходные тексты функций и последующей повторной компоновке. Плохо это или хорошо?
Для ответа на этот вопрос следует знать, что ссылка на экспортируемую функцию может выполняться двумя различными способами - по имени функции и по ее порядковому номеру. Если функция вызывается по имени, ее порядковый номер не имеет значения. Однако вызов функции по порядковому номеру выполняется быстрее, поэтому использование порядковых номеров предпочтительнее.
Если при помощи файла определения модуля DLL-библиотеки вы задаете фиксированное распределение порядковых номеров экспортируемых функций, при внесении изменений в исходные тексты DLL-библиотеки это распределение не изменится. В этом случае все приложения, ссылающиеся на функции из этой библиотеки по их порядковым номерам, будут работать правильно. Если же вы не определили порядковые номера функций, у приложений могут возникнуть проблемы с правильной адресацией функции из-за возможного изменения этих номеров.
Чтобы сказанное стало более понятным, рассмотрим подробнее механизм, с помощью которого приложения ссылаются на функции из DLL-библиотек.
Импортирование функций
Когда вы используете статическую компоновку, то включаете в файл проекта приложения соответствующий lib-файл, содержащий нужную вам библиотеку объектных модулей. Такая библиотека содержит исполняемый код модулей, который на этапе статической компоновки включается в exe-файл загрузочного модуля.
Если используется динамическая компоновка, в загрузочный exe-файл приложения записывается не исполнимый код функций, а ссылка на соответствующую DLL-библиотеку и функцию внутри нее. Как мы уже говорили, эта ссылка может быть организована с использованием либо имени функции, либо ее порядкового номера в DLL-библиотеке.
Откуда при компоновке приложения редактор связей узнает имя DLL-библиотеки, имя или порядковый номер экспортируемой функции? Для динамической компоновки функции из DLLбиблиотеки можно использовать различные способы.
Библиотека импорта
Для того чтобы редактор связей мог создать ссылку, в файл проекта приложения вы должны включить так называемую библиотеку импорта (import library ), созданную приложением Import Lib , входящим в состав системы разработки Borland Turbo C++ for Windows. Соответствующее средство имеется в SDK, а также в любой другой системе разработки приложений Windows.
Библиотека импорта создается либо на основе dll-файла библиотеки, либо на основе файла определения модуля, используемого для создания DLL-библиотеки.
В любом случае вам надо запустить приложение Import Lib, и из меню "File" выбрать строку "File Select...". При этом на экране появится диалоговая панель "Select File", с помощью которой вы можете выбрать нужный вам dllили def-файл (рис. 3.6).
Рис. 3.6. Работа с приложением Import Library
После выбора файла приложение Import Lib создаст библиотеку импорта в виде lib-файла, расположенного в том же каталоге, что и исходный dllили def-файл. Этот файл необходимо включить в проект создаваемого вами приложения, пользующегося функциями DLL-библиотеки.
Следует заметить, что стандартные библиотеки систем разработки приложений Windows содержат как обычные объектные модули, предназначенные для статической компоновки, так и ссылки на различные стандартные DLL-библиотеки, экспортирующие функции программного интерфейса операционной системы Windows.
В состав системы разработки Borland C++ for Windows версии 4.0 и 4.01 входит DLL-библиотека, содержащая функции стандартной библиотеки компилятора. Эта библиотека расположена в файле с именем bc40rtl.dll. В проекте приложения вы можете определить, что для стандартных функций следует использовать динамическую компоновку. В этом случае размер полученного загрузочного модуля заметно сократится, однако для работы приложения будет нужен файл bc40rtl.dll.
Использование оператора IMPORTS
Использование библиотек импорта - удобный, но не единственный способ компоновки функций DLL-библиотеки. Этот способ можно рассматривать как способ неявной компоновки на этапе редактирования загрузочного модуля приложения, так как имена импортируемых функций нигде не перечисляются.
Другой способ основан на использовании в файле определения модуля приложения оператора IMPORTS . С помощью этого оператора можно перечислить импортируемые функции, указав имена функций или их порядковые номера , а также имена файлов соответствующих DLLбиблиотек:
IMPORTS Msg=dllsrc.4 dllsrc.TellMe
Во второй строке приведенного выше примера приложение импортирует функцию Msg из DLLбиблиотеки dllsrc.dll, причем порядковый номер указанной функции в библиотеке равен 4.
В третьей строке из DLL-библиотеки dllsrc.dll импортируется функция с именем TellMe, причем ее порядковый номер не используется.
Для увеличения скорости работы приложения рекомендуется использовать способ, основанный на ссылке по имени DLL-библиотеки и порядковому номеру функции внутри этой библиотеки.
Динамический импорт функций во время выполнения приложения
В некоторых случаях невозможно выполнить динамическую компоновку на этапе редактирования. Вы можете, например, создать приложение, которое состоит из основного модуля и дополнительных, реализованных в виде DLL-библиотек. Состав этих дополнительных модулей и имена файлов, содержащих DLL-библиотеки, может изменяться, при этом в приложение могут добавляться новые возможности.
Если вы, например, разрабатываете систему распознавания речи, то можете сделать ее в виде основного приложения и набора DLL-библиотек, по одной библиотеке для каждого национального языка. В продажу система может поступить в комплекте с одной или двумя библиотеками, но в дальнейшем пользователь сможет купить дополнительные библиотеки и, просто переписав новые библиотеки на диск, получить возможность работы с другими языками. При этом основной модуль приложения не может "знать" заранее имена файлов дополнительных DLL-библиотек, поэтому динамическая компоновка с использованием библиотеки импорта или оператора IMPORTS файла определения модуля невозможна.
Однако приложение может в любой момент времени загрузить любую DLL-библиотеку, вызвав специально предназначенную для этого функцию программного интерфейса Windows с именем LoadLibrary . Приведем ее прототип:
HINSTANCE WINAPI LoadLibrary(LPCSTR lpszLibFileName);
Параметр функции является указателем на текстовую строку, закрытую двоичным нулем. В эту строку перед вызовом функции следует записать путь к файлу DLL-библиотеки или имя этого файла. Если путь к файлу не указан, при поиске выполняется последовательный просмотр следующих каталогов:
текущий каталога;
каталог, в котором находится операционная система Windows;
системный каталог Windows;
каталог, в котором находится загрузочный файл приложения, загружающего DLL-библиотеку;
каталоги, перечисленные в операторе описания среды PATH, расположенном в файле autoexec.bat;
каталоги, расположенные в сети, если для них определены пути поиска.
Если файл DLL-библиотеки найден, функция LoadLibrary возвращает идентификатор модуля библиотеки. В противном случае возвращается код ошибки, который по своему значению меньше величины HINSTANCE_ERROR, определенной в файле windows.h. Возможны следующие коды ошибок:
Код ошибки
0
2
3
5

Описание

Мало памяти, неправильный формат загружаемого файла

Файл не найден 
Путь к файлу не существует
Была предпринята попытка динамически загрузить приложение или произошла ошибка при совместном использовании файлов. Также возможно, что была предпринята попытка доступа к файлу в сети пользователем, не имеющим на это достаточных прав
6
8
10
11
12
13
14
15
16
19
20
21
Данная библиотека требует отдельный сегмент данных для каждой задачи

Мало памяти для запуска приложения

Неправильная версия операционной системы Windows 
Неправильный формат загрузочного файла приложения Windows
Данное приложение разработано для операционной системы,
отличной от Microsoft Windows

Данное приложение разработано для MS-DOS версии 4.0 
Неизвестный тип исполняемого файла
Была предпринята попытка загрузить приложение, разработанное для реального режима работы процессора в среде ранних версий операционной системы Windows
Была предпринята попытка загрузить вторую копию исполняемого файла, содержащего сегменты данных, отмеченные как multiple, но не защищенные от записи

Попытка загрузки компрессованного выполнимого файла

Файл, содержащий DLL-библиотеку, имеет неправильный формат
Данное приложение работает только в среде 32-битового расширения Windows
Функция LoadLibrary может быть вызвана разными приложениями для одной и той же DLLбиблиотеки несколько раз. В этом случае загрузка DLL-библиотеки выполняется только один раз. Последующие вызовы функции LoadLibrary приводят только к увеличению счетчика использования DLL-библиотеки.
В качестве примера приведем фрагмент исходного текста приложения, загружающего DLLбиблиотеку из файла srcdll.dll:
HINSTANCE hLib;
hLib = LoadLibrary("srcdll.dll"); if(hLib >= HINSTANCE_ERROR)
{
// Работа с DLL-библиотекой
}
FreeLibrary(hLib);
Если DLL-библиотека больше не нужна, ее следует освободить с помощью функции FreeLibrary :
void WINAPI FreeLibrary(HINSTANCE hLibrary);
В качестве параметра этой функции следует передать идентификатор освобождаемой библиотеки.
Если счетчик использования DLL-библиотеки становится равным нулю (что происходит, когда все приложения, работавшие с библиотекой, освободили ее или завершили свою работу), DLLбиблиотека выгружается из памяти. При этом вызывается функция WEP, выполняющая все необходимые завершающие действия.
Однако прежде чем выгружать библиотеку из памяти, приложение, вероятно, захочет вызвать из нее хотя бы одну функцию, поэтому теперь мы расскажем о том, как вызвать функцию из загруженной DLL-библиотеки.
Для того чтобы вызвать функцию из библиотеки, зная ее идентификатор, необходимо получить значение дальнего указателя на эту функцию, вызвав функцию GetProcAddress :
FARPROC WINAPI GetProcAddress(HINSTANCE hLibrary,
LPCSTR lpszProcName);
Через параметр hLibrary вы должны передать функции идентификатор DLL-библиотеки, полученный ранее от функции LoadLibrary.
Параметр lpszProcName является дальним указателем на строку, содержащую имя функции или ее порядковый номер, преобразованный макрокомандой MAKEINTRESOURCE.
Приведем фрагмент кода, в котором определяются адреса двух функций. В первом случае используется имя функции, а во втором - ее порядковый номер:
FARPROC lpMsg; FARPROC lpTellMe;
lpMsg = GetProcAddress(hLib, "Msg");
lpTellMe = GetProcAddress(hLib, MAKEINTRESOURCE(8));
Перед тем как передать управление функции по полученному адресу, следует убедиться в том, что этот адрес не равен NULL:
if(lpMsg != (FARPROC)NULL)
{
(*lpMsg)((LPSTR)"My message");
}
Для того чтобы включить механизм проверки типов передаваемых параметров, вы можете определить свой тип - указатель на функцию, и затем использовать его для преобразования типа адреса, полученного от функции GetProcAddress:
typedef int (FAR PASCAL *LPGETZ)(int x, int y); LPGETZ lpGetZ;
lpGetZ = (LPGETZ)GetProcAddress(hLib, "GetZ");
А что произойдет, если приложение при помощи функции LoadLibrary попытается загрузить DLL-библиотеку, которой нет на диске?
В этом случае операционная система Windows выведет на экран диалоговую панель с сообщением о том, что она не может найти нужную DLL-библиотеку. В некоторых случаях появление такого сообщения нежелательно, так как либо вас не устраивает внешний вид этой диалоговой панели, либо по логике работы вашего приложения описанная ситуация является нормальной.
Для того чтобы отключить режим вывода диалоговой панели с сообщением о невозможности загрузки DLL-библиотеки, вы можете использовать функцию SetErrorMode , передав ей в качестве параметра значение SEM_NOOPENFILEERRORBOX .
Приведем прототип функции SetErrorMode:
UINT WINAPI SetErrorMode(UINT fuErrorMode);
Эта функция позволяет отключать встроенный в Windows обработчик прерывания MS-DOS INT 24h (критическая ошибка). В качестве параметра этой функции можно указывать комбинацию следующих значений:
|
|
|
Значение |
|
Описание |
|
|
|
|
|
Операционная система Windows не |
|
|
выводит на экран сообщение обработчика |
SEM_FAILCRITICALERRORS |
|
критических ошибок, возвращая |
|
|
приложению соответствующий код |
|
|
ошибки |
|
|
|
|
|
На экран не выводится сообщение об |
|
|
ошибке защиты памяти. Этот флаг может |
SEM_NOGPFAULTERRORBOX |
|
использоваться только при отладке |
|
|
приложений, если они имеют собственный |
|
|
обработчик такой ошибки |
|
|
|
|
|
Если Windows не может открыть файл, на |
SEM_NOOPENFILEERRORBOX |
|
экран не выводится диалоговая панель с |
|
|
сообщением об ошибке |
|
|
|
Функция SetErrorMode возвращает предыдущий режим обработки ошибки.
Файл определения модуля для DLL-библиотеки
Файл определения модуля для DLL-библиотеки отличается от соответствующего файла обычного приложения Windows. В качестве примера приведем образец такого файла:
LIBRARY |
DLLNAME |
|
DESCRIPTION 'DLL-библиотека DLLNAME' |
||
EXETYPE |
windows |
|
CODE |
moveable discardable |
|
DATA |
moveable single |
|
HEAPSIZE |
|
1024 |
EXPORTS |
|
|
DrawBitmap @4 |
||
ShowAll |
|
|
HideAll |
|
|
GetMyPool |
@8 |
|
FreeMyPool |
@9 |
|
В файле определения модуля DLL-библиотеки вместо оператора NAME должен находиться оператор LIBRARY , определяющий имя модуля DLL-библиотеки, под которым она будет известна Windows.
Остальные операторы те же, что и для обычного приложения, за исключением того что в файле определения модуля DLL-библиотеки не должно быть оператора STACKSIZE (так как у DLLбиблиотеки нет стека).
Оператор CODE используется для определения атрибутов сегмента кода DLL-библиотеки.
Особенностью оператора DATA является использование параметра SINGLE. Так как DLLбиблиотека находится в памяти в единственном экземпляре и может иметь только один сегмент данных, для описания сегмента данных библиотеки требуется параметр SINGLE.
Оператор HEAPSIZE определяет начальное значение локальной области памяти, которая будет выделена для DLL-библиотеки функцией LibEntry на этапе инициализации. Если DLL-библиотека не должна иметь локальную область данных, в качестве параметра следует указать нулевое значение:
HEAPSIZE 0
И, наконец, оператор EXPORTS предназначен для перечисления имен и порядковых номеров функций, экспортируемых DLL-библиотекой.
Работа с библиотеками динамической компоновки (DLL) при их создании в различных средах разработки.
С самого рождения (или чуть позже) операционная система windows использовала библиотеки динамической компоновки dll (dynamic link library), в которых содержались реализации наиболее часто применяемых функций. Наследники windows - nt и windows 95, а также os/2 - тоже зависят от библиотек dll в плане обеспечения значительной части их функциональных возможностей.
Рассмотрим ряд аспектов создания и использования библиотек dll:
как статически подключать библиотеки dll; как динамически загружать библиотеки dll; как создавать библиотеки dll;
как создавать расширения Мfc библиотек dll. Использование dll
Практически невозможно создать приложение windows, в котором не использовались бы библиотеки dll. В dll содержатся все функции win32 api и несчетное количество других функций операционных систем win32.
Вообще говоря, dll - это просто наборы функций, собранные в библиотеки. Однако, в отличие от своих статических родственников (файлов . lib), библиотеки dll не присоединены непосредственно к выполняемым файлам с помощью редактора связей. В выполняемый файл занесена только информация об их местонахождении. В момент выполнения программы загружается вся библиотека целиком. Благодаря этому разные процессы могут пользоваться совместно одними и теми же библиотеками, находящимися в памяти. Такой подход позволяет сократить объем памяти, необходимый для нескольких приложений, использующих много общих библиотек, а также контролировать размеры ЕХЕ-файлов.
Однако, если библиотека используется только одним приложением, лучше сделать ее обычной, статической. Конечно, если входящие в ее состав функции будут использоваться только в одной программе, можно просто вставить в нее соответствующий файл с исходным текстом.
Чаще всего проект подключается к dll статически, или неявно, на этапе компоновки. Загрузкой dll при выполнении программы управляет операционная система. Однако, dll можно загрузить и явно, или динамически, в ходе работы приложения.
Библиотеки импортирования.
При статическом подключении dll имя .lib-файла определяется среди прочих параметров редактора связей в командной строке или на вкладке "link" диалогового окна "project settings" среды developer
studio. Однако .lib-файл, используемый при неявном подключении dll, - это не обычная статическая библиотека. Такие .lib-файлы называются библиотеками импортирования (import libraries). В них содержится не сам код библиотеки, а только ссылки на все функции, экспортируемые из файла dll, в котором все и хранится. В результате библиотеки импортирования, как правило, имеют меньший размер, чем dll-файлы. К способам их создания вернемся позднее. А сейчас рассмотрим другие вопросы, касающиеся неявного подключения динамических библиотек.
Согласование интерфейсов.
При использовании собственных библиотек или библиотек независимых разработчиков придется обратить внимание на согласование вызова функции с ее прототипом.
Если бы мир был совершенен, то программистам не пришлось бы беспокоиться о согласовании интерфейсов функций при подключении библиотек - все они были бы одинаковыми. Однако мир далек от совершенства, и многие большие программы написаны с помощью различных библиотек без c++.
По умолчанию в visual c++ интерфейсы функций согласуются по правилам c++. Это значит, что параметры заносятся в стек справа налево, вызывающая программа отвечает за их удаление из стека при выходе из функции и расширении ее имени. Расширение имен (name mangling) позволяет редактору связей различать перегруженные функции, т.е. функции с одинаковыми именами, но разными списками аргументов. Однако в старой библиотеке С функции с расширенными именами отсутствуют.
Хотя все остальные правила вызова функции в С идентичны правилам вызова функции в c++, в библиотеках С имена функций не расширяются. К ним только добавляется впереди символ подчеркивания (_).
Если необходимо подключить библиотеку на С к приложению на c++, все функции из этой библиотеки придется объявить как внешние в формате С:
extern "С" int myoldcfunction(int myparam);
Объявления функций библиотеки обычно помещаются в файле заголовка этой библиотеки, хотя заголовки большинства библиотек С не рассчитаны на применение в проектах на c++. В этом случае необходимо создать копию файла заголовка и включить в нее модификатор extern "c" к объявлению всех используемых функций библиотеки. Модификатор extern "c" можно применить и к целому блоку, к которому с помощью директивы #include подключен файл старого заголовка С. Таким образом, вместо модификации каждой функции в отдельности можно обойтись всего тремя строками:
extern "С"
{
#include "myclib.h"
}
В программах для старых версий windows использовались также соглашения о вызове функций языка pascal для функций windows api. В новых программах следует использовать модификатор winapi, преобразуемый в _stdcall. Хотя это и не стандартный интерфейс функций С или c++, но именно он используется для обращений к функциям windows api. Однако обычно все это уже учтено в стандартных заголовках windows.
Загрузка неявно подключаемой dll
При запуске приложение пытается найти все файлы dll, неявно подключенные к приложению, и поместить их в область оперативной памяти, занимаемую данным процессом. Поиск файлов dll операционной системой осуществляется в следующей последовательности.
Каталог, в котором находится ЕХЕ-файл. Текущий каталог процесса.
Системный каталог windows.
Если библиотека dll не обнаружена, приложение выводит диалоговое окно с сообщением о ее отсутствии и путях, по которым осуществлялся поиск. Затем процесс отключается.
Если нужная библиотека найдена, она помещается в оперативную память процесса, где и остается до его окончания. Теперь приложение может обращаться к функциям, содержащимся в dll.
Динамическая загрузка и выгрузка dll
Вместо того, чтобы windows выполняла динамическое связывание с dll при первой загрузке приложения в оперативную память, можно связать программу с модулем библиотеки во время выполнения программы
(при таком способе в процессе создания приложения не нужно использовать библиотеку импорта). В частности, можно определить, какая из библиотек dll доступна пользователю, или разрешить пользователю выбрать, какая из библиотек будет загружаться. Таким образом можно использовать разные dll, в которых реализованы одни и те же функции, выполняющие различные действия. Например, приложение, предназначенное для независимой передачи данных, сможет в ходе выполнения принять решение, загружать ли dll для протокола tcp/ip или для другого протокола.
Загрузка обычной dll.
Первое, что необходимо сделать при динамической загрузке dll, - это поместить модуль библиотеки в память процесса. Данная операция выполняется с помощью функции ::loadlibrary, имеющей единственный аргумент - имя загружаемого модуля. Соответствующий фрагмент программы должен выглядеть так:
hinstance hmydll;
::
if((hmydll=::loadlibrary("mydll"))==null) { /* не удалось загрузить dll */ }
else { /* приложение имеет право пользоваться функциями dll через hmydll */ }
Стандартным расширением файла библиотеки windows считает .dll, если не указать другое расширение. Если в имени файла указан и путь, то только он будет использоваться для поиска файла. В противном случае windows будет искать файл по той же схеме, что и в случае неявно подключенных dll, начиная с каталога, из которого загружается exe-файл, и продолжая в соответствии со значением path.
Когда windows обнаружит файл, его полный путь будет сравнен с путем библиотек dll, уже загруженных данным процессом. Если обнаружится тождество, вместо загрузки копии приложения возвращвется дескриптор уже подключенной библиотеки.
Если файл обнаружен и библиотека успешно загрузилась, функция ::loadlibrary возвращает ее дескриптор, который используется для доступа к функциям библиотеки.
Перед тем, как использовать функции библиотеки, необходимо получить их адрес. Для этого сначала следует воспользоваться директивой typedef для определения типа указателя на функцию и определить переменую этого нового типа, например:
//тип pfn_myfunction будет объявлять указатель на функцию,
//принимающую указатель на символьный буфер и выдающую значение типа int typedef int (winapi *pfn_myfunction)(char *);
::
pfn_myfunction pfnmyfunction;
Затем следует получить дескриптор библиотеки, при помощи которого и определить адреса функций, например адрес фунции с именем myfunction:
hmydll=::loadlibrary("mydll"); pfnmyfunction=(pfn_myfunction)::getprocaddress(hmydll,"myfunction");
::
int icode=(*pfnmyfunction)("hello");
Адрес функции определяется при помощи функции ::getprocaddress, ей следует передать имя библиотеки и имя функции. Последнее должно передаваться в том виде, в котором эксаортируется из dll.
Можно также сослаться на функцию по порядковому номеру, по которому она экспортируется (при этом для создания библиотеки должен использоваться def-файл, об этом будет рассказано далее):
pfnmyfunction=(pfn_myfunction)::getprocaddress(hmydll, makeintresource(1));
После завершения работы с библиотекой динамической компоновки, ее можно выгрузить из памяти процесса с помощью функции ::freelibrary:
::freelibrary(hmydll);
Загрузка mfc-расширений динамических библиотек
При загрузке mfc-расширений для dll (подробно о которых рассказывается далее) вместо функций
