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

Тема в разделе "Программирование / Скриптинг", создана пользователем Kr1kuzya, 29 авг 2017.

  1. Kr1kuzya

    Kr1kuzya Костылизируя некостылизируемое Ньюсмейкер

    Сообщения:
    2.710
    Симпатии:
    2.350
    Введение
    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(hDirectoryszOtherFilesizeof szOtherFileeFileType)) {
            if (
    eFileType == FileType_File && // если тип прочитанного объекта из папки - файл
              
    strcmp(szOtherFileszFiletrue) == 0) { // и его имя совпадает
                
    return true;
            }
        }

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

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

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

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

    Клонирование объектов
    Есть некоторые проблемы с объектами, если один плагин делится с другим своим объектом. Простейший пример:
    • Плагин А создаёт некий объект.
    • Плагин Б получает указатель на объект от плагина А.
    • Плагин А выгружается из памяти, и все его указатели (включая переданный указатель в плагин Б) становятся недействительными.
    • Плагин Б пытается использовать указатель.
    Для предотвращения таких ситуаций, существует функция CloneHandle(), которая создаёт новый указатель на объект, который находится по переданному указателю. При использовании этой функции, можно так же передать указатель на плагин-владелец, которому новый указатель будет принадлежать.
    Вообще, есть два способа клонирования.
    1. Когда плагин Б получает уже склонированный указатель на объект от плагина А.
      PHP:
      /**
       * В плагине А
       */
      public Handle GetGlobalArray(Handle hOwner null) {
          return 
      CloneHandle(g_hArrayhOwner); // если на функцию передаётся 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)
    • Тип: IDatabase
    • Закрываемый: Да.
    • Клонируемый: Да.
    • 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().
    • Владелец объекта таймера был выгружен из памяти.

    Статья будет дорабатываться со временем.
     
    T1MOXA, Серый™, Someone и 10 другим нравится это.