Введение в программирование на SourcePawn. Часть 7.

Сообщения
207
Реакции
420
Помог
10 раз(а)
Введение в программирование на SourcePawn. Указатели (Handles).

Указатели (Handles) - своеобразные указатели из C++. Они позволяют плагину вручную контролировать свою память, и по сути своей, могут хранить в себе что угодно: соединения с базой данных, динамические по размеру массивы (которые увеличиваются по мере необходимости), стеки, структуры KeyValues, файлы, директории, консольные переменные и многое другое.
На указателях сборщик мусора SourcePawn работает не полноценно. Место от переменной с указателем он освобождает, но сам указатель не трогает.
Указатели имеют кей-ворд Handle. Некоторые указатели имеют свой уникальный кей-ворд (Database, File, ConVar и т.д.), который по сути наследуется от Handle и реализует классо-подобный доступ к функционалу или свойствам.
Стандартные типы указателей, которые предоставляет нам SourceMod, описаны в этой статье. Здесь же мы попробуем разобраться, что такое указатель сам по себе, и как с ним правильнее всего работать.

Создание указателей
Для создания указателя есть лишь один путь: вызвать функцию, которая его создаёт. Для каждого типа указателей она своя. Так же есть некий шанс того, что указатель не будет создан. В таком случае функция возвращает null, или INVALID_HANDLE (старый синтаксис; в принципе это одно и то же).
Есть виды указателей, которые SourceMod нам даст закрывать, а есть те, которые не даст.
Так же есть клонирование. Тут то же самое: что-то нам можно клонировать, что-то нет.

Рассмотрим создание указателя на соединение с локальной базой данных.
Чтобы проинициализировать соединение с БД, мы имеем две функции. Рассмотрим в нашем случае одну простейшую, которая гарантированно возвращает указатель на SQLite БД, а другие рассмотрим в тематическом уроке. Функция называется SQLite_UseDatabase(). Она принимает на вход название БД, указатель на буфер для ошибки и его размер.
Если соединение у функции не удаётся проинициализировать, она возвращает null, и записывает в переданный буфер причину ошибки.
скрин из API.png

Напишем следующую функцию, которая создаст нам БД (если её до сих пор нет), или отрапортует об ошибке, если такая произойдёт.
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 уничтожает все указатели при выгрузке плагина, он уничтожит и объект, на который ссылается указатель, если на него нет других указателей, принадлежащих другим плагинам. Клонирование в нашем случае позволит избежать типикал случая:
  • Плагин А отдаёт плагину В свой указатель
  • Плагин А выгружается
  • Плагин В пытается использовать указатель
В случае с не склонированным указателем, мы словим ошибку с кодом 3 ("Указатель не существует"). Если же мы склонируем, мы сможем взаимодействовать с объектом как ни в чём не бывало даже после выгрузки плагина.
Для клонирования, применяется функция 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 как со своими указателем и не бояться выгрузки плагина-ядра.
}
Ещё раз. Закрепим одну главную мысль. Клонирование указателей не клонирует объект. Оно создаёт ещё один указатель, ссылающийся на тот же объект. Если все указатели станут недействительны, объект уничтожится.
 

Пользователи, просматривающие эту тему

Сейчас на форуме нет ни одного пользователя.
Сверху Снизу