[SourcePawn] Пара слов о типе переменных Handle

Kruzya

Raspberry Pi 4
Команда форума
Меценат
Сообщения
10,550
Реакции
8,726
Введение
Handle - специальный тип переменных, используемый в SourcePawn. Является ссылкой (указателем) на некоторый объект из C или C++. У него есть счётчик упоминаний в других плагинах. Пока этот счётчик не равен нулю, SourceMod не уничтожит объект из своей памяти.
Поскольку SourcePawn не имеет сборщика мусора, объекты Handle - своеобразный способ дать плагинам вручную контролировать свою память.
Заметка: потеряв указатель на объект, он не закроется до тех пор, пока все ссылки (Handle) не будут закрыты. Все ссылки на объекты автоматически перестают быть валидными при выгрузке плагина, или ручном закрытии оной (оператор delete, или ф-ия CloseHandle()).

Открытие плагином нового объекта Handle
По факту, объекты открываются функциями ядра/расширений. Для примера, ф-ия OpenDirectory() возвращает Handle папки (тип Directory), из которого мы можем вытянуть список имеющихся там файлов, папок, и прочих объектов (сокеты UNIX) с помощью нативов, предназначенных для работы с папкой.
PHP:
Handle hMaps = OpenDirectory("maps");
Заметка: Пытаться работать с Handle папки функциями типа ArrayList, например, не получится: ядро проверяет тип объекта, и в случае несовпадения, возвращает ошибку.

Закрытие объектов
Уничтожение указателя на объект вручную
Уничтожение указателя может повлечь за собой полное удаление оригинального объекта из памяти, если это будет последний указатель. Для примера, уничтожение указателя на объект соединения с базой (тип IDatabase), повлечёт за собой закрытие соединения, если больше не будет указателей на это соединение. Указатель можно закрыть с помощью оператора языка delete, или функции CloseHandle().
PHP:
// Так:
delete g_hDatabase;
// Или так:
CloseHandle(g_hDatabase);

// Закрытие указателя не обнуляет его в переменной, потому если Вы где-то отслеживаете, не равна ли переменная null или INVALID_HANDLE, то имеет смысл приравнять её к null после закрытия.
g_hDatabase = null;

Уничтожение всех указателей плагина при выгрузке последнего
Когда плагин выгружается из памяти сервера, все указатели автоматически становятся невалидными, закрытыми. Это не означает, что закрытие указателей опционально. Возьмём для примера следующий код:
PHP:
stock bool FileExistsInFolder(const char[] szPath, const char[] szFile) {
    Handle hDirectory = OpenDirectory(szPath);
    if (hDirectory == null) { // При открытии хендла, так же может произойти неудача, потому имеет смысл проверять результат
        return false;
    }

    char szOtherFile[PLATFORM_MAX_PATH];
    FileType eFileType;
    while (ReadDirEntry(hDirectory, szOtherFile, sizeof szOtherFile, eFileType)) {
        if (eFileType == FileType_File && // если тип прочитанного объекта из папки - файл
          strcmp(szOtherFile, szFile, true) == 0) { // и его имя совпадает
            return true;
        }
    }

    delete hDirectory;
    return false;
}
И так, что здесь? Функция ищет в папке некоторый файл, и если находит, возвращает true, не закрывая указатель на Directory объект. В итоге происходит так называемая утечка памяти каждый раз, когда мы проверяем, есть ли нужный нам файл в некоторой папке. Как итог, у плагина копятся указатели на незакрытые объекты.

И так, когда мы должны уничтожать указатели? Сформируем два правила:
  • Если описание натива использует терминологию вроде "Открывает", "Создаёт" или похожие. Как правило, в описании так же пишется, что открытый указатель надо закрывать.
  • Если сам объект, на который ссылается указатель, используется временно для некоторых целей. Простейший пример: передача нескольких данных на функцию с помощью объекта типа DataPack.
Чаще всего, Вы обязаны закрывать указатели на объекты.

Когда мы не можем уничтожать указатели?
Есть пара моментов, когда мы не можем уничтожать указатели. Самый банальный пример: тип Plugin. Мы его не можем закрыть, он принадлежит ядру. Мы можем лишь вытягивать информацию из него.
Так же, один плагин не может закрывать указатели другого плагина. Это своеобразная защита от недочётов в API и прочих ошибок. Если Вам надо дать возможность закрыть указатель другим плагином, склонируйте объект на имя нужного плагина (подробнее ниже), свой указатель закройте, и отдайте указатель клона.

О счётчике указателей
Закрытие указателей на объект не всегда означает, что сам объект сразу же будет уничтожен. Пока в памяти есть указатели на него, он будет жить. Как только кол-во указателей достигнет нуля, он будет уничтожен.

Клонирование объектов
Есть некоторые проблемы с объектами, если один плагин делится с другим своим объектом. Простейший пример:
  • Плагин А создаёт некий объект.
  • Плагин Б получает указатель на объект от плагина А.
  • Плагин А выгружается из памяти, и все его указатели (включая переданный указатель в плагин Б) становятся недействительными.
  • Плагин Б пытается использовать указатель.
Для предотвращения таких ситуаций, существует функция CloneHandle(), которая создаёт новый указатель на объект, который находится по переданному указателю. При использовании этой функции, можно так же передать указатель на плагин-владелец, которому новый указатель будет принадлежать.
Вообще, есть два способа клонирования.
  1. Когда плагин Б получает уже склонированный указатель на объект от плагина А.
    PHP:
    /**
    * В плагине А
    */
    public Handle GetGlobalArray(Handle hOwner = null) {
    return CloneHandle(g_hArray, hOwner); // если на функцию передаётся null, то владельцем становится плагин, который клонирование и инициирует.
    }
    
    /**
    * В плагине Б
    */
    Handle hArray = GetGlobalArray(GetMyHandle());
    /* работаем с ним */
    delete hArray;
  2. Когда плагин Б получает оригинальный указатель на объект от плагина А, и вручную клонирует.
    PHP:
    /**
    * В плагине А
    */
    public Handle GetGlobalArray() {
    return g_hArray;
    }
    
    /**
    * В плагине Б
    */
    Handle hArray = CloneHandle(GetGlobalArray()); // если второй аргумент не передаётся, то используется стандартное значение по умолчанию (т.е., null).
    /* работаем с ним */
    delete hArray;

Типы объектов
Внизу представлены краткие описания стандартных типов объектов, представляемые самим SourceMod. Другие расширения могут создавать свои типы.

BitBuffers
  • Тип: bf_write или bf_read
  • Закрываемый: Только если разрешено.
  • Клонируемый: Только если его можно закрывать.
  • API: Ядро.
  • Упоминается в bitbuffer.inc
Существует четыре типа объектов BitBuffer. Они разделяются на bf_write (записываемый буфер) и bf_read (читаемый буфер). Они непосредственно наследовуются от их аналогов движка Half-Life 2. Внутренне для каждого из них есть отдельные типы объектов. Некоторые функции будут использовать объекты BitBuffer, которые не могут быть освобождены.

Консольные переменные
  • Тип: ConVar
  • Закрываемый: Нет.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в convars.inc
Объекты консольных переменных используются для чтения и записи значений консольных переменных. Они не могут быть закрыты, пока SourceMod не будет выгружен.

Пакеты данных (DataPack)
  • Тип: DataPack
  • Закрываемый: Да.
  • Клонируемый: Да.
  • API: Ядро.
  • Упоминается в datapack.inc
Объекты пакетов данных используются для упаковки, распаковки данных, записанных в определённом порядке. У них нельзя указать направление операций чтения/записи.
Объекты пакетов данных могут создаваться расширениями.

Папки (Directories)
  • Тип: Directory
  • Закрываемый: Да.
  • Клонируемый: Да.
  • API: Ядро.
  • Упоминается в files.inc
Объекты папок используются для получения содержимого папки в файловой системе.

Драйвера Базы данных (Database Drivers)
  • Тип: DBDriver
  • Закрываемый: Да.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в dbi.inc
Объекты драйверов содержат в себе информацию о драйвере БД. Они статичны, и не могут быть закрыты плагинами.

Запросы к Базе данных (Database Queries)
  • Тип: IQuery / IPreparedQuery
  • Закрываемый: Да.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в dbi.inc
Объекты запросов к базе являются оболочкой над результатами запросов самих драйверов. Закрытие оных освобождает все занятые ресурсы, включая информацию о самом запросе и ответ БД.

Соединение с Базой данных (Databases)
  • Тип: IDatabase
  • Закрываемый: Да.
  • Клонируемый: Да.
  • API: Ядро.
  • Упоминается в dbi.inc
Объект соединения с базой данных является оболочкой над объектом самого драйвера. Закрытие оного вызывает отключение от сервера и освобождение всех занятых ресурсов.

События (Events)
  • Тип: Event
  • Закрываемый: Нет.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в events.inc
Объекты событий используются для получения и установки данных события движка Source перед, или после вызова оного. Он может быть закрыт только с помощью CancelCreatedEvent(), если само событие ещё не было вызвано по некоторым причинам.

Файлы (Files)
  • Тип: File
  • Закрываемый: Да.
  • Клонируемый: Да.
  • API: Ядро.
  • Упоминается в files.inc
Объект открытого файла. Используется для чтения, записи, создания файлов в файловой системе.

Forwards
  • Тип: Forward
  • Закрываемый: Да.
  • Клонируемый: Только если разрешено.
  • API: Ядро.
  • Упоминается в functions.inc
Объекты форвардов используются для вызова функций из других плагинов. Всего существует два типа форвардов: глобальные и приватные. Только приватные форварды могут быть склонированы.

Структура Ключ-Значение (KeyValues)
  • Тип: KeyValues
  • Закрываемый: Да.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в keyvalues.inc
Объект структуры KeyValues. Наследуется сам по себе от класса движка KeyValues, чьё API и используется. Является само по себе древовидным, рекурсивным хранилищем некоторых данных для перечисления свойств или конфигурации.

Плагины (Plugins)
  • Тип: Plugin
  • Закрываемый: Нет.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в sourcemod.inc
Объекты плагинов используются для идентификации плагинов. Они принадлежат ядру, и не могут быть закрыты или склонированы плагинами или расширениями. Однако, плагины могут использовать объекты плагинов для получения информации о плагинах.
Указатели на плагины не должны храниться глобально в памяти плагина, т.к. плагин может быть выгружен, и его указатель станет недействительным.

Перечисления плагинов (Plugin Iterators)
  • Тип: PluginIter
  • Закрываемый: Да.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в sourcemod.inc
Перечисления плагинов позволят Вам пройтись по всем загруженным плагинам. Используются временно, потому должны быть закрыты после работы с ними.

Protobuf
  • Тип: protobuf
  • Закрываемый: Нет.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в protobuf.inc
В данный момент, объекты типа protobuf используются только в сетевом общении сервера с клиентом посредством User Messages в некоторых движках (например, Counter-Strike: Global Offensive). Объекты данного типа принадлежат и управляются ядром, и не могут быть закрыты/склонированы плагинами.

Синтаксические SMC-анализаторы (SMC Parsers)
  • Тип: SMC Parser
  • Закрываемый: Да.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в textparse.inc
Объекты SMC-анализаторов содержат в себе набор функций для обратного вызова при обработке чтения файла. Поскольку в данный момент, они ссылаются на функции внутри плагина, объекты данного типа не могут быть клонированы.

Таймеры (Timers)
  • Тип: Timer
  • Закрываемый: Да.
  • Клонируемый: Нет.
  • API: Ядро.
  • Упоминается в timers.inc
Таймеры являются временными объектами, которые могут быть закрыты при следующих событиях:
  • Таймер закончил своё выполнение (через возвращение значения Plugin_Stop, или при единичном выполнении (если одноразовый));
  • Таймер был убит с помощью KillTimer()
  • Все указатели на объект таймера были уничтожены посредством конструкции языка delete или функции CloseHandle().
  • Владелец объекта таймера был выгружен из памяти.

Статья будет дорабатываться со временем.
 
Последнее редактирование:

inklesspen

Пишу модули под LSD
Меценат
Сообщения
1,687
Реакции
696
upload_2017-10-29_11-40-54.png
Опечатка, тут тип File

-э.. Дабавлено позже, спустя минуту-
2 месяца не могли заметить..
 

R1KO

fuck society
Команда форума
Сообщения
9,016
Реакции
6,897
Хотелось бы сказать пару слов. Часто задают одни и те же вопросы вроде "когда нужно закрывать хендл?" "а что если не закрою?" и т.д.

Постараюсь сформулировать несколько правил:
  • Если плагин постоянно работает с хендлом то его не нужно закрывать. Один раз создаем (например в OnPluginStart) и работаем с ним. При завершении работы плагина SM сам закрывает все хендлы связанные с плагином. Например, так работают с базой данных - один раз подключились и всё. Повторное подключение можно сделать только при его потере.
  • Если хендл нужно периодически обновлять (например какой-то kv с игроками или настройками) - открываем при старте карты и работаем. Закрываем либо при старте карты перед открытием (не забыть проверить валидность) либо в конце карты. Если нужно обновление по команде - точно так же: закрыли активный, и открыли заново.
  • Еще бывает часто используются глобальные массивы (adt array) и карты (adt trie). Удобнее создавать их 1 раз в OnPluginStart, а затем просто очищать, например, перед загрузкой в них данных из конфигов в OnMapStart, а не пересоздавать их каждый раз.
  • Если используется хендл для каждого игрока ( массив хендлов ) то лучше полностью удалять хендл при выходе игрока и создавать при входе. Т.к. даже пустой хендл (имеется ввиду что хендл записан пустой объект, массив например) - использует некоторое количество памяти.
 
Последнее редактирование:

MAGNAT2645

Участник
Сообщения
64
Реакции
7
На этом форуме сказано, что delete приравнивает дескриптор к null. Здесь же об этом не упоминается. Так это правда или ложь? Просто мне лень самому проверить это :ab:

P.S. С НГ вас
 
Последнее редактирование:

Kruzya

Raspberry Pi 4
Команда форума
Меценат
Сообщения
10,550
Реакции
8,726
@MAGNAT2645, с определённого апдейта - приравнивается.
 

MAGNAT2645

Участник
Сообщения
64
Реакции
7
Т.е. по сути, с версии 1.9 можно считать, что приравнивает? Если да, то нужно будет воспользоваться этим, чтобы чуточку оптимизировать код (просто delete везде использую, и только сейчас понял, что я 2 раза приравниваю к null)
 

Kruzya

Raspberry Pi 4
Команда форума
Меценат
Сообщения
10,550
Реакции
8,726
@MAGNAT2645, можно и так сказать.
Убедиться в том, что приравнивание уже встроено, можно сделать путём дизассемблирования плагина.
 

nyood

e-val
Сообщения
1,119
Реакции
750
Как мне кажется, тему стоит закрепить в разделе и дополнить ее, если есть на то время.
 
Сверху