Разработчик
Проверенный
Участник
Пользователь
- Сообщения
- 207
- Реакции
- 420
- Помог
- 10 раз(а)
Введение в программирование на SourcePawn. Указатели (Handles).
Указатели (Handles) - своеобразные указатели из C++. Они позволяют плагину вручную контролировать свою память, и по сути своей, могут хранить в себе что угодно: соединения с базой данных, динамические по размеру массивы (которые увеличиваются по мере необходимости), стеки, структуры KeyValues, файлы, директории, консольные переменные и многое другое.
На указателях сборщик мусора SourcePawn работает не полноценно. Место от переменной с указателем он освобождает, но сам указатель не трогает.
Указатели имеют кей-ворд
Handle
. Некоторые указатели имеют свой уникальный кей-ворд (Database
, File
, ConVar
и т.д.), который по сути наследуется от Handle
и реализует классо-подобный доступ к функционалу или свойствам.Стандартные типы указателей, которые предоставляет нам SourceMod, описаны в этой статье. Здесь же мы попробуем разобраться, что такое указатель сам по себе, и как с ним правильнее всего работать.
Создание указателей
Для создания указателя есть лишь один путь: вызвать функцию, которая его создаёт. Для каждого типа указателей она своя. Так же есть некий шанс того, что указатель не будет создан. В таком случае функция возвращает
null
, или INVALID_HANDLE
(старый синтаксис; в принципе это одно и то же).Есть виды указателей, которые SourceMod нам даст закрывать, а есть те, которые не даст.
Так же есть клонирование. Тут то же самое: что-то нам можно клонировать, что-то нет.
Рассмотрим создание указателя на соединение с локальной базой данных.
Чтобы проинициализировать соединение с БД, мы имеем две функции. Рассмотрим в нашем случае одну простейшую, которая гарантированно возвращает указатель на SQLite БД, а другие рассмотрим в тематическом уроке. Функция называется
SQLite_UseDatabase()
. Она принимает на вход название БД, указатель на буфер для ошибки и его размер.Если соединение у функции не удаётся проинициализировать, она возвращает
null
, и записывает в переданный буфер причину ошибки.Напишем следующую функцию, которая создаст нам БД (если её до сих пор нет), или отрапортует об ошибке, если такая произойдёт.
C++:
Database g_hDb;
Database GetDB() {
if (g_hDb == null) {
char szError[256];
g_hDb = SQLite_UseDatabase("dev-cs", szError, sizeof(szError));
if (g_hDb == null)
SetFailState("Database failure: %s", szError);
}
return g_hDb;
}
Вроде всё выглядит красиво, всё хорошо. Мы можем общаться с БД через функции. связанные с ней. Но что-то здесь не так... Что именно?
На самом деле, эта функция имеет большую проблему. Хотя она и не очевидна, но однажды созданный указатель никогда не будет закрыт. На это в принципе наплевать, если функция хранит указатель глобально, но что делать, если оно нигде указатель не хранит и создаёт всё новые и новые указатели?
Их надо закрывать после того, как мы ими воспользовались.
Уничтожение указателей
Уничтожить указатель мы можем либо с помощью
CloseHandle()
, либо кей-ворда delete
.Поскольку закрытие указателя не обнуляет переменную, она продолжает на что-то ссылаться. Потому принято после закрытия указателя приравнять переменную, хранящую его - к
null
.Если бы мы писали на C++ и попытались обратиться по уже несуществующему указателю, мы бы словили Segmentation fault (обращение в участок памяти, который нам уже не принадлежит), после которого ОС убивает наше приложение. SourceMod нас предохраняет частично от ситуаций, когда мы можем убить сервер своей кривой работой с указателями. Помимо того, что он следит за их кол-вом, он ещё и выгружает Ваш плагин при достижении некого предела (~20000 указателей) с уничтожением всех указателей.
Имеет смысл отметить, что
delete
объединяет в себе и CloseHandle()
, и приравнивание переменной к null
, что освобождает нас от написания целой строчки кода. Однако бывают случаи, когда это будет лишним и лучше использовать по старинке CloseHandle()
.А теперь напишем код, который выполнит некий запрос к базе в другом потоке и закроет соединение с БД (на самом деле, оно закроется не сразу, а после выполнения запроса, но это тема отдельного разговора):
Код:
Database hDb = GetDB();
hDb.Query(DB_OnUserFetched, "SELECT * FROM `xf_users`;");
// неоптимально, если мы в дальнейшем нигде не сравниваем переменную с null и она локальная, т.к. будет лишний вызов к виртуальной машине
delete hDb;
// оптимальнее всего в нашем случае, т.к. указатель мы нигде в дальнейшем не сравниваем.
CloseHandle(hDb);
Если в описании функции, которая открывает указатель, явно указано, что указатель надо закрыть, то не забывайте уничтожать его. Самые банальные примеры, когда указатель надо закрывать после работы с ним - папки, файлы, структуры KeyValues, БД.
Клонирование указателей
Клонирование указателей позволяет нам поделиться некоторым объектом с другим плагином, например. При клонировании мы можем указать владельца указателя. Так как SourceMod уничтожает все указатели при выгрузке плагина, он уничтожит и объект, на который ссылается указатель, если на него нет других указателей, принадлежащих другим плагинам. Клонирование в нашем случае позволит избежать типикал случая:
- Плагин А отдаёт плагину В свой указатель
- Плагин А выгружается
- Плагин В пытается использовать указатель
Для клонирования, применяется функция
CloneHandle()
. Она принимает на себя всего два аргумента:- Указатель на нужный объект, который должен быть склонирован
- Указатель на плагин, которому должен принадлежать новый указатель. Если не передать, используется дефолтное значение
null
, обозначающий, что новый владелец тот, кто инициирует клонирование указателя
Для примера напишем небольшую функцию-натив (рассмотрим, что это, в одном из других уроков), которая всегда возвращает новый указатель на соединение с БД, уже принадлежащий плагину, который вызвал его.
Код:
// плагин А
public int Native_GetDB(Handle hPlugin, int iNumParams) {
if (g_hDb == null)
return null;
return CloneHandle(g_hDb, hPlugin);
}
// плагин B
public void OnAllPluginsLoaded() {
g_hDB = LK_GetDB();
// теперь можно работать с g_hDB как со своими указателем и не бояться выгрузки плагина-ядра.
}